2 次代碼提交 84d3850b40 ... 401925d032

作者 SHA1 備註 提交日期
  Clément Wolf 401925d032 Fix replay playback; use /web/osu-getreplay.php endpoint 1 月之前
  Clément Wolf 6ff9452244 Update protocol version to b20240411.1 1 月之前

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

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

+ 6 - 1
src/App/Osu/BanchoLeaderboard.cpp

@@ -148,6 +148,9 @@ char *strtok_x(char d, char **str) {
 }
 
 void process_leaderboard_response(Packet response) {
+    // Don't update the leaderboard while playing, that's weird
+    if(bancho.osu->isInPlayMode()) return;
+
     // NOTE: We're not doing anything with the "info" struct.
     //       Server can return partial responses in some cases, so make sure
     //       you actually received the data if you plan on using it.
@@ -192,7 +195,9 @@ void process_leaderboard_response(Packet response) {
 
     char *score_line = NULL;
     while((score_line = strtok_x('\n', &body))[0] != '\0') {
-        scores.push_back(parse_score(score_line));
+        OsuDatabase::Score score = parse_score(score_line);
+        score.md5hash = beatmap_hash;
+        scores.push_back(score);
     }
 
     // XXX: We should also separately display either the "personal best" the server sent us,

+ 54 - 17
src/App/Osu/BanchoNetworking.cpp

@@ -12,6 +12,7 @@
 #include "Downloader.h"
 #include "Engine.h"
 #include "File.h"
+#include "OsuBeatmap.h"
 #include "OsuChat.h"
 #include "OsuDatabase.h"
 #include "OsuLobby.h"
@@ -96,7 +97,6 @@ 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);
@@ -158,6 +158,8 @@ size_t curl_write(void *contents, size_t size, size_t nmemb, void *userp) {
 }
 
 static void send_api_request(CURL *curl, APIRequest api_out) {
+    // XXX: Use download()
+
     Packet response;
     response.id = api_out.type;
     response.extra = api_out.extra;
@@ -366,22 +368,57 @@ static void *do_networking(void *data) {
 }
 
 static void handle_api_response(Packet packet) {
-    if(packet.id == GET_MAP_LEADERBOARD) {
-        process_leaderboard_response(packet);
-    } else if(packet.id == GET_BEATMAPSET_INFO) {
-        OsuRoom::process_beatmapset_info_response(packet);
-    } else if(packet.id == MARK_AS_READ) {
-        // (nothing to do)
-    } else if(packet.id == SUBMIT_SCORE) {
-        // TODO @kiwec: handle response
-        debugLog("Score submit result: %s\n", packet.memory);
-
-        // Reset leaderboards so new score will appear
-        bancho.osu->m_songBrowser2->m_db->m_online_scores.clear();
-        bancho.osu->m_songBrowser2->rebuildScoreButtons();
-    } else {
-        // NOTE: API Response type is same as API Request type
-        debugLog("No handler for API response type %d!\n", packet.id);
+    switch(packet.id) {
+        case GET_BEATMAPSET_INFO: {
+            OsuRoom::process_beatmapset_info_response(packet);
+            break;
+        }
+
+        case GET_MAP_LEADERBOARD: {
+            process_leaderboard_response(packet);
+            break;
+        }
+
+        case GET_REPLAY: {
+            auto replay_frames = OsuReplay::get_frames(packet.memory, packet.size);
+            if(replay_frames.empty()) {
+                // Most likely, 404
+                bancho.osu->m_notificationOverlay->addNotification("Failed to download replay");
+                break;
+            }
+
+            bancho.osu->replay_info = *((ReplayExtraInfo *)packet.extra);
+            auto diff2_md5 = bancho.osu->replay_info.diff2_md5;
+            auto beatmap = bancho.osu->getSongBrowser()->getDatabase()->getBeatmapDifficulty(diff2_md5);
+            if(beatmap == nullptr) {
+                // XXX: Auto-download beatmap
+                bancho.osu->m_notificationOverlay->addNotification("Missing beatmap for this replay");
+            } else {
+                bancho.osu->getSongBrowser()->onDifficultySelected(beatmap, false);
+                bancho.osu->getSelectedBeatmap()->watch(replay_frames);
+            }
+            break;
+        }
+
+        case MARK_AS_READ: {
+            // (nothing to do)
+            break;
+        }
+
+        case SUBMIT_SCORE: {
+            // TODO @kiwec: handle response
+            debugLog("Score submit result: %s\n", packet.memory);
+
+            // Reset leaderboards so new score will appear
+            bancho.osu->getSongBrowser()->m_db->m_online_scores.clear();
+            bancho.osu->getSongBrowser()->rebuildScoreButtons();
+            break;
+        }
+
+        default: {
+            // NOTE: API Response type is same as API Request type
+            debugLog("No handler for API response type %d!\n", packet.id);
+        }
     }
 }
 

+ 11 - 3
src/App/Osu/BanchoNetworking.h

@@ -14,12 +14,13 @@
 #define MCOSU_UPDATE_URL "https://mcosu.kiwec.net"
 
 // NOTE: Full version can be something like "b20200201.2cuttingedge"
-#define OSU_VERSION "b20240330.2"
-#define OSU_VERSION_DATEONLY 20240330
+#define OSU_VERSION "b20240411.1"
+#define OSU_VERSION_DATEONLY 20240411
 
 enum APIRequestType {
-    GET_MAP_LEADERBOARD,
     GET_BEATMAPSET_INFO,
+    GET_MAP_LEADERBOARD,
+    GET_REPLAY,
     MARK_AS_READ,
     SUBMIT_SCORE,
 };
@@ -32,6 +33,13 @@ struct APIRequest {
     uint32_t extra_int = 0;  // lazy
 };
 
+struct ReplayExtraInfo {
+    MD5Hash diff2_md5;
+    int32_t mod_flags;
+    UString username;
+    int32_t player_id;
+};
+
 void disconnect();
 void reconnect();
 

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

@@ -76,7 +76,7 @@ void submit_score(OsuDatabase::Score score) {
     {
         part = curl_mime_addpart(request.mime);
         curl_mime_name(part, "bmk");
-        curl_mime_data(part, score.diff2->getMD5Hash().toUtf8(), CURL_ZERO_TERMINATED);
+        curl_mime_data(part, score.md5hash.hash, CURL_ZERO_TERMINATED);
     }
     {
         auto unique_ids = UString::format("%s|%s", bancho.install_id.toUtf8(), bancho.disk_uuid.toUtf8());

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

@@ -230,9 +230,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);
+    mirror.append(UString::format("%d", set_id));
     int response_code = 0;
-    download(url.toUtf8(), progress, data, &response_code);
+    download(mirror.toUtf8(), progress, data, &response_code);
     if(response_code != 200) return;
 
     // Download succeeded: save map to disk

+ 42 - 99
src/App/Osu/Osu.cpp

@@ -131,7 +131,6 @@ ConVar osu_force_legacy_slider_renderer("osu_force_legacy_slider_renderer", fals
                                         "on some older machines, this may be faster than vertexbuffers");
 
 ConVar osu_draw_fps("osu_draw_fps", true, FCVAR_NONE);
-ConVar osu_hide_cursor_during_gameplay("osu_hide_cursor_during_gameplay", false, FCVAR_NONE);
 
 ConVar osu_alt_f4_quits_even_while_playing("osu_alt_f4_quits_even_while_playing", true, FCVAR_NONE);
 ConVar osu_win_disable_windows_key_while_playing("osu_win_disable_windows_key_while_playing", true, FCVAR_NONE);
@@ -149,13 +148,13 @@ ConVar mp_password("mp_password", "", FCVAR_NONE);
 ConVar mp_autologin("mp_autologin", false, FCVAR_NONE);
 ConVar submit_scores("submit_scores", false, FCVAR_NONE);
 
-// Some alternative mirrors:
-// - https://api.osu.direct/d/%d
-// - https://chimu.moe/d/%d
-// - https://api.nerinyan.moe/d/%d
-// - https://osu.gatari.pw/d/%d
-// - https://osu.sayobot.cn/osu.php?s=%d
-ConVar beatmap_mirror("beatmap_mirror", "https://catboy.best/s/%d", FCVAR_NONE,
+// If catboy.best doesn't work for you, here are some alternatives:
+// - https://api.osu.direct/d/
+// - https://chimu.moe/d/
+// - https://api.nerinyan.moe/d/
+// - https://osu.gatari.pw/d/
+// - https://osu.sayobot.cn/osu.php?s=
+ConVar beatmap_mirror("beatmap_mirror", "https://catboy.best/s/", FCVAR_NONE,
                       "mirror from which beatmapsets will be downloaded");
 
 ConVar *Osu::version = &osu_version;
@@ -598,17 +597,15 @@ void Osu::draw(Graphics *g) {
     if(isBufferedDraw) m_backBuffer->enable();
 
     // draw everything in the correct order
-    if(isInPlayMode())  // if we are playing a beatmap
-    {
+    if(isInPlayMode()) {  // if we are playing a beatmap
         OsuBeatmap *beatmap = getSelectedBeatmap();
-        const bool isAuto = (m_bModAuto || m_bModAutopilot);
         const bool isFPoSu = (m_osu_mod_fposu_ref->getBool());
 
         if(isFPoSu) m_playfieldBuffer->enable();
 
         getSelectedBeatmap()->draw(g);
 
-        if(m_bModFlashlight && beatmap != NULL) {
+        if(m_bModFlashlight) {
             // Dim screen when holding a slider
             float max_opacity = 1.f;
             if(holding_slider && !avoid_flashes.getBool()) {
@@ -616,7 +613,7 @@ void Osu::draw(Graphics *g) {
             }
 
             // Convert screen mouse -> osu mouse pos
-            Vector2 cursorPos = isAuto ? beatmap->getCursorPos() : engine->getMouse()->getPos();
+            Vector2 cursorPos = beatmap->getCursorPos();
             Vector2 mouse_position = cursorPos - OsuGameRules::getPlayfieldOffset(this);
             mouse_position /= OsuGameRules::getPlayfieldScaleFactor(this);
 
@@ -656,19 +653,6 @@ void Osu::draw(Graphics *g) {
             g->fillRect(0, 0, getScreenWidth(), getScreenHeight());
         }
 
-        // special cursor handling (fading cursor + invisible cursor mods + draw order etc.)
-        const bool allowDoubleCursor = (env->getOS() == Environment::OS::OS_HORIZON || isFPoSu);
-        const bool allowDrawCursor = (!osu_hide_cursor_during_gameplay.getBool() || getSelectedBeatmap()->isPaused());
-        float fadingCursorAlpha =
-            1.0f - clamp<float>((float)m_score->getCombo() / osu_mod_fadingcursor_combo.getFloat(), 0.0f, 1.0f);
-        if(m_pauseMenu->isVisible() || getSelectedBeatmap()->isContinueScheduled()) fadingCursorAlpha = 1.0f;
-
-        // draw auto cursor
-        if(isAuto && allowDrawCursor && !isFPoSu && beatmap != NULL && !beatmap->isLoading())
-            m_hud->drawCursor(
-                g, m_osu_mod_fps_ref->getBool() ? OsuGameRules::getPlayfieldCenter(this) : beatmap->getCursorPos(),
-                osu_mod_fadingcursor.getBool() ? fadingCursorAlpha : 1.0f);
-
         m_pauseMenu->draw(g);
         m_modSelector->draw(g);
         m_chat->draw(g);
@@ -681,9 +665,12 @@ void Osu::draw(Graphics *g) {
         if(isFPoSu && m_osu_draw_cursor_ripples_ref->getBool()) m_hud->drawCursorRipples(g);
 
         // draw FPoSu cursor trail
+        float fadingCursorAlpha =
+            1.0f - clamp<float>((float)m_score->getCombo() / osu_mod_fadingcursor_combo.getFloat(), 0.0f, 1.0f);
+        if(m_pauseMenu->isVisible() || getSelectedBeatmap()->isContinueScheduled() || !osu_mod_fadingcursor.getBool())
+            fadingCursorAlpha = 1.0f;
         if(isFPoSu && m_fposu_draw_cursor_trail_ref->getBool())
-            m_hud->drawCursorTrail(g, beatmap->getCursorPos(),
-                                   osu_mod_fadingcursor.getBool() ? fadingCursorAlpha : 1.0f);
+            m_hud->drawCursorTrail(g, beatmap->getCursorPos(), fadingCursorAlpha);
 
         if(isFPoSu) {
             m_playfieldBuffer->disable();
@@ -694,20 +681,15 @@ void Osu::draw(Graphics *g) {
         }
 
         // draw player cursor
-        if((!isAuto || allowDoubleCursor) && allowDrawCursor) {
-            Vector2 cursorPos = (beatmap != NULL && !isAuto) ? beatmap->getCursorPos() : engine->getMouse()->getPos();
-
-            if(isFPoSu) {
-                cursorPos = getScreenSize() / 2.0f;
-            }
-
-            const bool updateAndDrawTrail = !isFPoSu;
-
-            m_hud->drawCursor(g, cursorPos, (osu_mod_fadingcursor.getBool() && !isAuto) ? fadingCursorAlpha : 1.0f,
-                              isAuto, updateAndDrawTrail);
+        Vector2 cursorPos = beatmap->getCursorPos();
+        bool drawSecondTrail = (m_bModAuto || m_bModAutopilot || beatmap->m_bIsWatchingReplay);
+        bool updateAndDrawTrail = true;
+        if(isFPoSu) {
+            cursorPos = getScreenSize() / 2.0f;
+            updateAndDrawTrail = false;
         }
-    } else  // if we are not playing
-    {
+        m_hud->drawCursor(g, cursorPos, fadingCursorAlpha, drawSecondTrail, updateAndDrawTrail);
+    } else {  // if we are not playing
         m_lobby->draw(g);
         m_room->draw(g);
 
@@ -1050,46 +1032,6 @@ 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()) {
@@ -1139,32 +1081,33 @@ void Osu::update() {
 
 void Osu::updateMods() {
     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;
+        // XXX: clear experimental mods
+        m_bModNC = replay_info.mod_flags & ModFlags::Nightcore;
+        m_bModDT = replay_info.mod_flags & ModFlags::DoubleTime && !m_bModNC;
+        m_bModHT = replay_info.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_bModNF = replay_info.mod_flags & ModFlags::NoFail;
+        m_bModEZ = replay_info.mod_flags & ModFlags::Easy;
+        m_bModTD = replay_info.mod_flags & ModFlags::TouchDevice;
+        m_bModHD = replay_info.mod_flags & ModFlags::Hidden;
+        m_bModHR = replay_info.mod_flags & ModFlags::HardRock;
+        m_bModSD = replay_info.mod_flags & ModFlags::SuddenDeath;
+        m_bModRelax = replay_info.mod_flags & ModFlags::Relax;
+        m_bModAutopilot = replay_info.mod_flags & ModFlags::Autopilot;
+        m_bModAuto = replay_info.mod_flags & ModFlags::Autoplay && !m_bModAutopilot;
+        m_bModSpunout = replay_info.mod_flags & ModFlags::SpunOut;
+        m_bModSS = replay_info.mod_flags & ModFlags::Perfect;
+        m_bModTarget = replay_info.mod_flags & ModFlags::Target;
+        m_bModScorev2 = replay_info.mod_flags & ModFlags::ScoreV2;
+        m_bModFlashlight = replay_info.mod_flags & ModFlags::Flashlight;
         m_bModNightmare = false;
 
-        osu_speed_override.setValue("0");
+        osu_speed_override.setValue("-1");
     } else if(bancho.is_in_a_multi_room()) {
         m_bModNC = bancho.room.mods & ModFlags::Nightcore;
         if(m_bModNC) {

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

@@ -9,8 +9,8 @@
 #define OSU_H
 
 #include "App.h"
+#include "BanchoNetworking.h"
 #include "MouseListener.h"
-#include "OsuReplay.h"
 
 class CWindowManager;
 
@@ -359,7 +359,7 @@ class Osu : public App, public MouseListener {
     CWindowManager *m_windowManager;
 
     // replay
-    OsuReplay::Info replay;
+    ReplayExtraInfo replay_info;
 
     // custom
     bool m_bScheduleEndlessModNextBeatmap;

+ 65 - 59
src/App/Osu/OsuBeatmap.cpp

@@ -712,9 +712,6 @@ bool OsuBeatmap::watch(std::vector<OsuReplay::Frame> replay) {
 
     m_bIsWatchingReplay = true;  // play() resets this to false
     spectated_replay = std::move(replay);
-    last_event_ms = -1;
-    last_keys = 0;
-    current_keys = 0;
 
     env->setCursorVisible(true);
 
@@ -1021,6 +1018,7 @@ void OsuBeatmap::stop(bool quit) {
 
 void OsuBeatmap::fail() {
     if(m_bFailed) return;
+    if(m_bIsWatchingReplay) return;
 
     // Change behavior of relax mod when online
     if(bancho.is_online() && m_osu->getModRelax()) return;
@@ -1094,8 +1092,10 @@ void OsuBeatmap::seekPercent(double percent) {
         onPlayStart();
     }
 
-    debugLog("Disabling score submission due to seeking\n");
-    vanilla = false;
+    if(!m_bIsWatchingReplay) {  // score submission already disabled when watching replay
+        debugLog("Disabling score submission due to seeking\n");
+        vanilla = false;
+    }
 }
 
 void OsuBeatmap::seekPercentPlayable(double percent) {
@@ -1663,9 +1663,10 @@ void OsuBeatmap::resetScore() {
     });
 
     last_event_time = engine->getTimeReal();
-    last_event_ms = 0;
+    last_event_ms = -1;
     current_keys = 0;
     last_keys = 0;
+    current_frame_idx = 0;
     m_iCurMusicPos = 0;
     m_iCurMusicPosWithOffsets = 0;
 
@@ -2438,60 +2439,62 @@ void OsuBeatmap::update2() {
     updateTimingPoints(m_iCurMusicPosWithOffsets);
 
     if(m_bIsWatchingReplay) {
-        OsuReplay::Frame current_frame = spectated_replay[0];
-        OsuReplay::Frame next_frame = spectated_replay[0];
-
-        for(auto frame : spectated_replay) {
-            next_frame = frame;
-
-            // XXX: The way this is done, inputs will be skipped if the user is lagging
-            if(next_frame.cur_music_pos > m_iCurMusicPosWithOffsets) {
-                if(current_frame.cur_music_pos > last_event_ms) {
-                    last_event_ms = current_frame.cur_music_pos;
-                    last_keys = current_keys;
-                    current_keys = current_frame.key_flags;
-
-                    // Released key 1
-                    if(last_keys & (OsuReplay::K1) && current_keys & ~(OsuReplay::K1)) {
-                        m_osu->getHUD()->animateInputoverlay(1, false);
-                    } else if(last_keys & (OsuReplay::M1) && current_keys & ~(OsuReplay::M1)) {
-                        m_osu->getHUD()->animateInputoverlay(3, false);
-                    }
+        OsuReplay::Frame current_frame = spectated_replay[current_frame_idx];
+        OsuReplay::Frame next_frame = spectated_replay[current_frame_idx + 1];
 
-                    // Released key 2
-                    if(last_keys & (OsuReplay::K2) && current_keys & ~(OsuReplay::K2)) {
-                        m_osu->getHUD()->animateInputoverlay(2, false);
-                    } else if(last_keys & (OsuReplay::M2) && current_keys & ~(OsuReplay::M2)) {
-                        m_osu->getHUD()->animateInputoverlay(4, false);
-                    }
+        while(next_frame.cur_music_pos <= m_iCurMusicPosWithOffsets) {
+            if(current_frame_idx + 2 >= spectated_replay.size()) break;
 
-                    // Pressed key 1
-                    if(last_keys & (OsuReplay::K1) && current_keys & ~(OsuReplay::K1)) {
-                        m_osu->getHUD()->animateInputoverlay(1, true);
-                        m_clicks.push_back(last_event_ms);
-                        if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(1);
-                    } else if(last_keys & (OsuReplay::M1) && current_keys & ~(OsuReplay::M1)) {
-                        m_osu->getHUD()->animateInputoverlay(3, true);
-                        m_clicks.push_back(last_event_ms);
-                        if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(3);
-                    }
+            last_keys = current_keys;
 
-                    // Pressed key 2
-                    if(last_keys & ~(OsuReplay::K2) && current_keys & (OsuReplay::K2)) {
-                        m_osu->getHUD()->animateInputoverlay(2, true);
-                        m_clicks.push_back(last_event_ms);
-                        if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(2);
-                    } else if(last_keys & ~(OsuReplay::M2) && current_keys & (OsuReplay::M2)) {
-                        m_osu->getHUD()->animateInputoverlay(4, true);
-                        m_clicks.push_back(last_event_ms);
-                        if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(4);
-                    }
-                }
+            current_frame_idx++;
+            current_frame = spectated_replay[current_frame_idx];
+            next_frame = spectated_replay[current_frame_idx + 1];
+            current_keys = current_frame.key_flags;
 
-                break;
+            // Flag fix to simplify logic (stable sets both K1 and M1 when K1 is pressed)
+            if(current_keys & OsuReplay::K1) current_keys &= ~OsuReplay::M1;
+            if(current_keys & OsuReplay::K2) current_keys &= ~OsuReplay::M2;
+
+            // Released key 1
+            if(last_keys & OsuReplay::K1 && !(current_keys & OsuReplay::K1)) {
+                m_osu->getHUD()->animateInputoverlay(1, false);
+            }
+            if(last_keys & OsuReplay::M1 && !(current_keys & OsuReplay::M1)) {
+                m_osu->getHUD()->animateInputoverlay(3, false);
+            }
+
+            // Released key 2
+            if(last_keys & OsuReplay::K2 && !(current_keys & OsuReplay::K2)) {
+                m_osu->getHUD()->animateInputoverlay(2, false);
+            }
+            if(last_keys & OsuReplay::M2 && !(current_keys & OsuReplay::M2)) {
+                m_osu->getHUD()->animateInputoverlay(4, false);
+            }
+
+            // Pressed key 1
+            if(!(last_keys & OsuReplay::K1) && current_keys & OsuReplay::K1) {
+                m_osu->getHUD()->animateInputoverlay(1, true);
+                m_clicks.push_back(current_frame.cur_music_pos);
+                if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(1);
+            }
+            if(!(last_keys & OsuReplay::M1) && current_keys & OsuReplay::M1) {
+                m_osu->getHUD()->animateInputoverlay(3, true);
+                m_clicks.push_back(current_frame.cur_music_pos);
+                if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(3);
             }
 
-            current_frame = frame;
+            // Pressed key 2
+            if(!(last_keys & OsuReplay::K2) && current_keys & OsuReplay::K2) {
+                m_osu->getHUD()->animateInputoverlay(2, true);
+                m_clicks.push_back(current_frame.cur_music_pos);
+                if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(2);
+            }
+            if(!(last_keys & OsuReplay::M2) && current_keys & OsuReplay::M2) {
+                m_osu->getHUD()->animateInputoverlay(4, true);
+                m_clicks.push_back(current_frame.cur_music_pos);
+                if(!m_bInBreak && !m_bIsInSkippableSection) m_osu->getScore()->addKeyCount(4);
+            }
         }
 
         float percent = 0.f;
@@ -2499,10 +2502,10 @@ void OsuBeatmap::update2() {
             long ms_since_last_frame = m_iCurMusicPosWithOffsets - current_frame.cur_music_pos;
             percent = (float)ms_since_last_frame / (float)next_frame.milliseconds_since_last_frame;
         }
-        m_interpolatedMousePos = Vector2{
-            .x = lerp(current_frame.x, next_frame.x, percent),
-            .y = lerp(current_frame.y, next_frame.y, percent),
-        };
+        m_interpolatedMousePos =
+            Vector2{lerp(current_frame.x, next_frame.x, percent), lerp(current_frame.y, next_frame.y, percent)};
+        m_interpolatedMousePos *= OsuGameRules::getPlayfieldScaleFactor(m_osu);
+        m_interpolatedMousePos += OsuGameRules::getPlayfieldOffset(m_osu);
     }
 
     // for performance reasons, a lot of operations are crammed into 1 loop over all hitobjects:
@@ -3350,7 +3353,7 @@ Vector2 OsuBeatmap::osuCoords2LegacyPixels(Vector2 coords) const {
 }
 
 Vector2 OsuBeatmap::getMousePos() const {
-    if(m_bIsWatchingReplay) {
+    if(m_bIsWatchingReplay && !m_bIsPaused) {
         return m_interpolatedMousePos;
     } else {
         return engine->getMouse()->getPos();
@@ -3528,6 +3531,9 @@ void OsuBeatmap::onBeforeStop(bool quit) {
         m_osu->getScore()->setPPv2(0.0f);
     }
 
+    m_bIsWatchingReplay = false;
+    spectated_replay.clear();
+
     debugLog("OsuBeatmap::onBeforeStop() done.\n");
 }
 

+ 4 - 2
src/App/Osu/OsuBeatmap.h

@@ -18,7 +18,8 @@ class OsuDatabaseBeatmap;
 class OsuBackgroundStarCacheLoader;
 class OsuBackgroundStarCalcHandler;
 
-struct OsuBeatmap {
+class OsuBeatmap {
+   public:
     friend class OsuBackgroundStarCacheLoader;
     friend class OsuBackgroundStarCalcHandler;
 
@@ -186,10 +187,11 @@ struct OsuBeatmap {
     uint8_t last_keys = 0;
 
     // replay replaying
-    // last_event_ms, current_keys, last_keys also reused
+    // current_keys, last_keys also reused
     std::vector<OsuReplay::Frame> spectated_replay;
     Vector2 m_interpolatedMousePos;
     bool m_bIsWatchingReplay = false;
+    long current_frame_idx = 0;
 
     // used by OsuHitObject children and OsuModSelector
     inline Osu *getOsu() const { return m_osu; }

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

@@ -39,6 +39,7 @@ OsuChangelog::OsuChangelog(Osu *osu) : OsuScreenBackable(osu) {
     latest.changes.push_back("- Added option to disable in-game scoreboard animations");
     latest.changes.push_back("- Fixed hitobjects being hittable after failing");
     latest.changes.push_back("- Removed VR support");
+    latest.changes.push_back("- Updated protocol and database version to b20240411.1");
     changelogs.push_back(latest);
 
     CHANGELOG v34_08;

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

@@ -12,6 +12,7 @@
 #include <fstream>
 
 #include "Bancho.h"  // md5
+#include "BanchoNetworking.h"
 #include "ConVar.h"
 #include "Engine.h"
 #include "File.h"
@@ -45,7 +46,7 @@ ConVar osu_folder_sub_songs("osu_folder_sub_songs", "Songs/", FCVAR_NONE);
 ConVar osu_folder_sub_skins("osu_folder_sub_skins", "Skins/", FCVAR_NONE);
 
 ConVar osu_database_enabled("osu_database_enabled", true, FCVAR_NONE);
-ConVar osu_database_version("osu_database_version", 20240330, FCVAR_NONE,
+ConVar osu_database_version("osu_database_version", OSU_VERSION_DATEONLY, FCVAR_NONE,
                             "maximum supported osu!.db version, above this will use fallback loader");
 ConVar osu_database_ignore_version_warnings("osu_database_ignore_version_warnings", false, FCVAR_NONE);
 ConVar osu_database_ignore_version("osu_database_ignore_version", false, FCVAR_NONE,

+ 9 - 3
src/App/Osu/OsuHUD.cpp

@@ -1533,11 +1533,17 @@ std::vector<SCORE_ENTRY> OsuHUD::getCurrentScores() {
             nb_slots++;
         }
 
-        const bool isUnranked = (m_osu->getModAuto() || (m_osu->getModAutopilot() && m_osu->getModRelax()));
         SCORE_ENTRY playerScoreEntry;
-        playerScoreEntry.name = (isUnranked ? "McOsu" : m_name_ref->getString());
+        if(m_osu->getModAuto() || (m_osu->getModAutopilot() && m_osu->getModRelax())) {
+            playerScoreEntry.name = "McOsu";
+        } else if(beatmap->m_bIsWatchingReplay) {
+            playerScoreEntry.name = m_osu->replay_info.username;
+            playerScoreEntry.player_id = m_osu->replay_info.player_id;
+        } else {
+            playerScoreEntry.name = m_name_ref->getString();
+            playerScoreEntry.player_id = bancho.user_id;
+        }
         playerScoreEntry.entry_id = 0;
-        playerScoreEntry.player_id = bancho.user_id;
         playerScoreEntry.combo = m_osu->getScore()->getComboMax();
         playerScoreEntry.score = m_osu->getScore()->getScore();
         playerScoreEntry.accuracy = m_osu->getScore()->getAccuracy();

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

@@ -13,8 +13,8 @@
 class Osu;
 class OsuUIAvatar;
 class OsuScore;
-class OsuScoreboardSlot;
 class OsuBeatmap;
+struct OsuScoreboardSlot;
 
 class McFont;
 class ConVar;

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

@@ -1380,7 +1380,6 @@ OsuOptionsMenu::OsuOptionsMenu(Osu *osu) : OsuScreenBackable(osu) {
     addCheckbox("Rainbow Sliders", convar->getConVarByName("osu_slider_rainbow"));
     addCheckbox("Rainbow Numbers", convar->getConVarByName("osu_circle_number_rainbow"));
     addCheckbox("SliderBreak Epilepsy", convar->getConVarByName("osu_slider_break_epilepsy"));
-    addCheckbox("Invisible Cursor", convar->getConVarByName("osu_hide_cursor_during_gameplay"));
     addCheckbox("Draw 300s", convar->getConVarByName("osu_hitresult_draw_300s"));
 
     addSection("Maintenance");

+ 49 - 36
src/App/Osu/OsuReplay.cpp

@@ -46,6 +46,53 @@ OsuReplay::BEATMAP_VALUES OsuReplay::getBeatmapValuesForModsLegacy(int modsLegac
     return v;
 }
 
+std::vector<OsuReplay::Frame> OsuReplay::get_frames(uint8_t* replay_data, int32_t replay_size) {
+    std::vector<OsuReplay::Frame> replay_frames;
+    if(replay_size <= 0) return replay_frames;
+
+    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);
+        return replay_frames;
+    }
+
+    long cur_music_pos = 0;
+    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);
+
+        if(frame.milliseconds_since_last_frame != -12345) {
+            cur_music_pos += frame.milliseconds_since_last_frame;
+            frame.cur_music_pos = cur_music_pos;
+            replay_frames.push_back(frame);
+        }
+    }
+
+end:
+    lzma_end(&strm);
+    return replay_frames;
+}
+
 OsuReplay::Info OsuReplay::from_bytes(uint8_t* data, int s_data) {
     OsuReplay::Info info;
 
@@ -77,45 +124,11 @@ OsuReplay::Info OsuReplay::from_bytes(uint8_t* data, int s_data) {
     info.timestamp = read_int64(&replay) / 10;
 
     int32_t replay_size = read_int32(&replay);
+    if(replay_size <= 0) return info;
     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:
+    info.frames = OsuReplay::get_frames(replay_data, replay_size);
     delete[] replay_data;
-    lzma_end(&strm);
 
     return info;
 }

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

@@ -58,5 +58,6 @@ BEATMAP_VALUES getBeatmapValuesForModsLegacy(int modsLegacy, float legacyAR, flo
                                              float legacyHP);
 
 Info from_bytes(uint8_t* data, int s_data);
+std::vector<Frame> get_frames(uint8_t* replay_data, int32_t replay_size);
 
 }  // namespace OsuReplay

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

@@ -14,7 +14,13 @@ OsuScoreboardSlot::OsuScoreboardSlot(SCORE_ENTRY score, int index) {
     m_index = index;
 }
 
-OsuScoreboardSlot::~OsuScoreboardSlot() { delete m_avatar; }
+OsuScoreboardSlot::~OsuScoreboardSlot() {
+    anim->deleteExistingAnimation(&m_fAlpha);
+    anim->deleteExistingAnimation(&m_fFlash);
+    anim->deleteExistingAnimation(&m_y);
+
+    delete m_avatar;
+}
 
 void OsuScoreboardSlot::draw(Graphics *g) {
     if(m_fAlpha == 0.f) return;

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

@@ -2770,6 +2770,8 @@ void OsuSongBrowser2::updateScoreBrowserLayout() {
 }
 
 void OsuSongBrowser2::rebuildScoreButtons() {
+    if(!isVisible()) return;
+
     // XXX: When online, it would be nice to scroll to the current user's highscore
 
     // reset

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

@@ -11,6 +11,7 @@
 
 #include "AnimationHandler.h"
 #include "Bancho.h"
+#include "BanchoNetworking.h"
 #include "ConVar.h"
 #include "Console.h"
 #include "Engine.h"
@@ -563,7 +564,21 @@ void OsuUISongBrowserScoreButton::onContextMenu(UString text, int id) {
     }
 
     if(id == 2) {
-        bancho.downloading_replay_id = m_score.online_score_id;
+        ReplayExtraInfo *info = (ReplayExtraInfo *)calloc(1, sizeof(ReplayExtraInfo));
+        info->diff2_md5 = m_score.md5hash.hash;
+        info->mod_flags = m_score.modsLegacy;
+        info->username = m_score.playerName;
+        info->player_id = m_score.player_id;
+
+        APIRequest request;
+        request.type = GET_REPLAY;
+        request.path = UString::format("/web/osu-getreplay.php?u=%s&h=%s&m=0&c=%d", bancho.username.toUtf8(),
+                                       bancho.pw_md5.toUtf8(), m_score.online_score_id);
+        request.mime = NULL;
+        request.extra = (uint8_t *)info;
+        send_api_request(request);
+
+        m_osu->m_notificationOverlay->addNotification("Downloading replay...");
         return;
     }