Quellcode durchsuchen

Implement replay downloading

Clément Wolf vor 1 Monat
Ursprung
Commit
84d3850b40

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

@@ -24,6 +24,8 @@ struct Bancho {
     MD5Hash pw_md5;
     Room room;
 
+    uint64_t downloading_replay_id = 0;
+
     bool prefer_daycore = false;
 
     ServerPolicy score_submission_policy = ServerPolicy::NO_PREFERENCE;

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

@@ -9,6 +9,7 @@
 #include "BanchoProtocol.h"
 #include "CBaseUICheckbox.h"
 #include "ConVar.h"
+#include "Downloader.h"
 #include "Engine.h"
 #include "File.h"
 #include "OsuChat.h"
@@ -95,6 +96,7 @@ void disconnect() {
     //      While offline ones would be "By score", "By pp", etc
     bancho.osu->m_songBrowser2->onSortScoresChange(UString("Sort By Score"), 0);
 
+    bancho.downloading_replay_id = 0;
     abort_downloads();
 
     pthread_mutex_unlock(&outgoing_mutex);

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

@@ -17,6 +17,7 @@ struct DownloadResult {
     std::string url;
     std::vector<uint8_t> data;
     float progress = 0.f;
+    int response_code = 0;
 };
 
 struct DownloadThread {
@@ -96,18 +97,19 @@ void* do_downloads(void* arg) {
         curl_easy_setopt(curl, CURLOPT_CAINFO, "curl-ca-bundle.crt");
 #endif
         CURLcode res = curl_easy_perform(curl);
+        int response_code;
+        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
         if(res == CURLE_OK) {
             threads_mtx.lock();
             result->progress = 1.f;
+            result->response_code = response_code;
             result->data = std::vector<uint8_t>(response.memory, response.memory + response.size);
             threads_mtx.unlock();
         } else {
             debugLog("Failed to download %s: %s\n", url.c_str(), curl_easy_strerror(res));
 
-            int response_code;
-            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
-
             threads_mtx.lock();
+            result->response_code = response_code;
             if(response_code == 429) {
                 result->progress = 0.f;
             } else {
@@ -144,7 +146,7 @@ end_thread:
     return NULL;
 }
 
-void download(const char* url, float* progress, std::vector<uint8_t>& out) {
+void download(const char* url, float* progress, std::vector<uint8_t>& out, int* response_code) {
     char* hostname = NULL;
     bool download_found = false;
     DownloadThread* matching_thread = nullptr;
@@ -192,6 +194,7 @@ void download(const char* url, float* progress, std::vector<uint8_t>& out) {
 
         if(result->url == url) {
             *progress = result->progress;
+            *response_code = result->response_code;
             if(result->progress == -1.f || result->progress == 1.f) {
                 out = result->data;
                 delete matching_thread->downloads[i];
@@ -228,8 +231,9 @@ void download_beatmapset(uint32_t set_id, float* progress) {
     std::vector<uint8_t> data;
     auto mirror = convar->getConVarByName("beatmap_mirror")->getString();
     auto url = UString::format(mirror.toUtf8(), set_id);
-    download(url.toUtf8(), progress, data);
-    if(*progress != 1.f) return;
+    int response_code = 0;
+    download(url.toUtf8(), progress, data, &response_code);
+    if(response_code != 200) return;
 
     // Download succeeded: save map to disk
     mz_zip_archive zip = {0};

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

@@ -6,7 +6,7 @@ void abort_downloads();
 // Downloads `url` and stores downloaded file data into `out`
 // When file is fully downloaded, `progress` is 1 and `out` is not NULL
 // When download fails, `progress` is -1
-void download(const char *url, float *progress, std::vector<uint8_t> &out);
+void download(const char *url, float *progress, std::vector<uint8_t> &out, int *response_code);
 
 // Downloads and extracts given beatmapset
 // When download/extraction fails, `progress` is -1

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

@@ -18,6 +18,7 @@
 #include "ConVar.h"
 #include "Console.h"
 #include "ConsoleBox.h"
+#include "Downloader.h"
 #include "Engine.h"
 #include "Environment.h"
 #include "Keyboard.h"
@@ -1049,6 +1050,46 @@ void Osu::update() {
     receive_api_responses();
     receive_bancho_packets();
 
+    // replay downloading
+    if(bancho.downloading_replay_id > 0) {
+        float progress = -1.f;
+        std::vector<uint8_t> replay_data;
+
+        // NOTE: Assuming the server doesn't require authentication :Clueless:
+        auto url =
+            UString::format("https://api.%s/get_replay?id=%d", bancho.endpoint.toUtf8(), bancho.downloading_replay_id);
+
+        int response_code = 0;
+        download(url.toUtf8(), &progress, replay_data, &response_code);
+
+        if(progress == -1.f || (progress == 1.f && response_code != 200)) {
+            bancho.downloading_replay_id = 0;
+            m_notificationOverlay->addNotification("Failed to download replay");
+            debugLog("Replay download: HTTP error %d\n", response_code);
+        } else if(progress < 1.f) {
+            auto progress_str = UString::format("Downloading replay... (%.1f%%)", progress * 100.f);
+            m_notificationOverlay->addNotification(progress_str);
+        } else {
+            // XXX: Undefined behavior if the user did random stuff before we start the replay
+            bancho.downloading_replay_id = 0;
+
+            replay = OsuReplay::from_bytes(replay_data.data(), replay_data.size());
+            if(replay.frames.empty()) {
+                m_notificationOverlay->addNotification("Replay file is corrupt or unavailable");
+            } else {
+                MD5Hash diff2_md5(replay.diff2_md5.toUtf8());
+                auto beatmap = getSongBrowser()->getDatabase()->getBeatmapDifficulty(diff2_md5);
+                if(beatmap == nullptr) {
+                    // XXX: Auto-download beatmap
+                    m_notificationOverlay->addNotification("Missing beatmap for this replay");
+                } else {
+                    m_songBrowser2->onDifficultySelected(beatmap, false);
+                    getSelectedBeatmap()->watch(replay.frames);
+                }
+            }
+        }
+    }
+
     // skin async loading
     if(m_bSkinLoadScheduled) {
         if(m_skinScheduledToLoad != NULL && m_skinScheduledToLoad->isReady()) {
@@ -1097,7 +1138,34 @@ void Osu::update() {
 }
 
 void Osu::updateMods() {
-    if(bancho.is_in_a_multi_room()) {
+    if(getSelectedBeatmap() != nullptr && getSelectedBeatmap()->m_bIsWatchingReplay) {
+        m_bModNC = replay.mod_flags & ModFlags::Nightcore;
+        m_bModDT = replay.mod_flags & ModFlags::DoubleTime && !m_bModNC;
+        m_bModHT = replay.mod_flags & ModFlags::HalfTime;
+        m_bModDC = false;
+        if(m_bModHT && bancho.prefer_daycore) {
+            m_bModHT = false;
+            m_bModDC = true;
+        }
+
+        m_bModNF = replay.mod_flags & ModFlags::NoFail;
+        m_bModEZ = replay.mod_flags & ModFlags::Easy;
+        m_bModTD = replay.mod_flags & ModFlags::TouchDevice;
+        m_bModHD = replay.mod_flags & ModFlags::Hidden;
+        m_bModHR = replay.mod_flags & ModFlags::HardRock;
+        m_bModSD = replay.mod_flags & ModFlags::SuddenDeath;
+        m_bModRelax = replay.mod_flags & ModFlags::Relax;
+        m_bModAutopilot = replay.mod_flags & ModFlags::Autopilot;
+        m_bModAuto = replay.mod_flags & ModFlags::Autoplay && !m_bModAutopilot;
+        m_bModSpunout = replay.mod_flags & ModFlags::SpunOut;
+        m_bModSS = replay.mod_flags & ModFlags::Perfect;
+        m_bModTarget = replay.mod_flags & ModFlags::Target;
+        m_bModScorev2 = replay.mod_flags & ModFlags::ScoreV2;
+        m_bModFlashlight = replay.mod_flags & ModFlags::Flashlight;
+        m_bModNightmare = false;
+
+        osu_speed_override.setValue("0");
+    } else if(bancho.is_in_a_multi_room()) {
         m_bModNC = bancho.room.mods & ModFlags::Nightcore;
         if(m_bModNC) {
             m_bModDT = false;

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

@@ -10,6 +10,7 @@
 
 #include "App.h"
 #include "MouseListener.h"
+#include "OsuReplay.h"
 
 class CWindowManager;
 
@@ -357,6 +358,9 @@ class Osu : public App, public MouseListener {
     // debugging
     CWindowManager *m_windowManager;
 
+    // replay
+    OsuReplay::Info replay;
+
     // custom
     bool m_bScheduleEndlessModNextBeatmap;
     int m_iMultiplayerClientNumEscPresses;

+ 10 - 1
src/App/Osu/OsuBeatmap.cpp

@@ -702,17 +702,26 @@ bool OsuBeatmap::watch(std::vector<OsuReplay::Frame> replay) {
         return false;
     }
 
+    m_bIsWatchingReplay = true;
+    m_osu->onBeforePlayStart();
+
     // Map failed to load
     if(!play()) {
         return false;
     }
 
+    m_bIsWatchingReplay = true;  // play() resets this to false
     spectated_replay = std::move(replay);
-    m_bIsWatchingReplay = true;
     last_event_ms = -1;
     last_keys = 0;
     current_keys = 0;
 
+    env->setCursorVisible(true);
+
+    m_osu->m_songBrowser2->m_bHasSelectedAndIsPlaying = true;
+    m_osu->m_songBrowser2->setVisible(false);
+    m_osu->onPlayStart();
+
     return true;
 }
 

+ 3 - 2
src/App/Osu/OsuChat.cpp

@@ -575,8 +575,9 @@ bool OsuChat::isVisibilityForced() {
 void OsuChat::updateVisibility() {
     auto selected_beatmap = m_osu->getSelectedBeatmap();
     bool can_skip = (selected_beatmap != nullptr) && (selected_beatmap->isInSkippableSection());
-    bool is_clicking_circles =
-        m_osu->isInPlayMode() && !can_skip && !m_osu->m_bModAuto && !m_osu->m_pauseMenu->isVisible();
+    bool is_spectating = m_osu->m_bModAuto || (m_osu->m_bModAutopilot && m_osu->m_bModRelax) ||
+                         (selected_beatmap != nullptr && selected_beatmap->m_bIsWatchingReplay);
+    bool is_clicking_circles = m_osu->isInPlayMode() && !can_skip && !is_spectating && !m_osu->m_pauseMenu->isVisible();
     if(bancho.is_playing_a_multi_map() && !bancho.room.all_players_loaded) {
         is_clicking_circles = false;
     }

+ 77 - 0
src/App/Osu/OsuReplay.cpp

@@ -7,7 +7,10 @@
 
 #include "OsuReplay.h"
 
+#include <lzma.h>
+
 #include "BanchoProtocol.h"
+#include "Engine.h"
 
 OsuReplay::BEATMAP_VALUES OsuReplay::getBeatmapValuesForModsLegacy(int modsLegacy, float legacyAR, float legacyCS,
                                                                    float legacyOD, float legacyHP) {
@@ -42,3 +45,77 @@ OsuReplay::BEATMAP_VALUES OsuReplay::getBeatmapValuesForModsLegacy(int modsLegac
 
     return v;
 }
+
+OsuReplay::Info OsuReplay::from_bytes(uint8_t* data, int s_data) {
+    OsuReplay::Info info;
+
+    Packet replay;
+    replay.memory = data;
+    replay.size = s_data;
+
+    info.gamemode = read_byte(&replay);
+    if(info.gamemode != 0) {
+        debugLog("Replay has unexpected gamemode %d!", info.gamemode);
+        return info;
+    }
+
+    info.osu_version = read_int32(&replay);
+    info.diff2_md5 = read_string(&replay);
+    info.username = read_string(&replay);
+    info.replay_md5 = read_string(&replay);
+    info.num300s = read_short(&replay);
+    info.num100s = read_short(&replay);
+    info.num50s = read_short(&replay);
+    info.numGekis = read_short(&replay);
+    info.numKatus = read_short(&replay);
+    info.numMisses = read_short(&replay);
+    info.score = read_int32(&replay);
+    info.comboMax = read_short(&replay);
+    info.perfect = read_byte(&replay);
+    info.mod_flags = read_int32(&replay);
+    info.life_bar_graph = read_string(&replay);
+    info.timestamp = read_int64(&replay) / 10;
+
+    int32_t replay_size = read_int32(&replay);
+    auto replay_data = new uint8_t[replay_size];
+    read_bytes(&replay, replay_data, replay_size);
+
+    lzma_stream strm = LZMA_STREAM_INIT;
+    lzma_ret ret = lzma_alone_decoder(&strm, UINT64_MAX);
+    if(ret != LZMA_OK) {
+        debugLog("Failed to init lzma library (%d).\n", ret);
+        delete[] replay_data;
+        return info;
+    }
+
+    std::stringstream ss;
+    std::string frame_str;
+    uint8_t outbuf[BUFSIZ];
+    strm.next_in = replay_data;
+    strm.avail_in = replay_size;
+    do {
+        strm.next_out = outbuf;
+        strm.avail_out = sizeof(outbuf);
+
+        ret = lzma_code(&strm, LZMA_FINISH);
+        if(ret != LZMA_OK && ret != LZMA_STREAM_END) {
+            debugLog("Decompression error (%d).\n", ret);
+            goto end;
+        }
+
+        ss.write((const char*)outbuf, sizeof(outbuf) - strm.avail_out);
+    } while(strm.avail_out == 0);
+
+    while(std::getline(ss, frame_str, ',')) {
+        OsuReplay::Frame frame;
+        sscanf(frame_str.c_str(), "%ld|%f|%f|%hhu", &frame.milliseconds_since_last_frame, &frame.x, &frame.y,
+               &frame.key_flags);
+        info.frames.push_back(frame);
+    }
+
+end:
+    delete[] replay_data;
+    lzma_end(&strm);
+
+    return info;
+}

+ 23 - 0
src/App/Osu/OsuReplay.h

@@ -33,7 +33,30 @@ struct BEATMAP_VALUES {
     float csDifficultyMultiplier;
 };
 
+struct Info {
+    uint8_t gamemode;
+    uint32_t osu_version;
+    UString diff2_md5;
+    UString username;
+    UString replay_md5;
+    int num300s;
+    int num100s;
+    int num50s;
+    int numGekis;
+    int numKatus;
+    int numMisses;
+    int32_t score;
+    int comboMax;
+    bool perfect;
+    int32_t mod_flags;
+    UString life_bar_graph;
+    int64_t timestamp;
+    std::vector<Frame> frames;
+};
+
 BEATMAP_VALUES getBeatmapValuesForModsLegacy(int modsLegacy, float legacyAR, float legacyCS, float legacyOD,
                                              float legacyHP);
 
+Info from_bytes(uint8_t* data, int s_data);
+
 }  // namespace OsuReplay

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

@@ -40,7 +40,8 @@ bool download_avatar(uint32_t user_id) {
     float progress = -1.f;
     std::vector<uint8_t> data;
     auto img_url = UString::format("https://a.%s/%d", bancho.endpoint.toUtf8(), user_id);
-    download(img_url.toUtf8(), &progress, data);
+    int response_code;
+    download(img_url.toUtf8(), &progress, data, &response_code);
     if(progress == -1.f) blacklist.push_back(user_id);
     if(data.empty()) return false;
 

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

@@ -10,6 +10,7 @@
 #include <chrono>
 
 #include "AnimationHandler.h"
+#include "Bancho.h"
 #include "ConVar.h"
 #include "Console.h"
 #include "Engine.h"
@@ -562,7 +563,7 @@ void OsuUISongBrowserScoreButton::onContextMenu(UString text, int id) {
     }
 
     if(id == 2) {
-        // TODO @kiwec: view replay
+        bancho.downloading_replay_id = m_score.online_score_id;
         return;
     }