Skip to content

Fix potentially incorrect online play beatmap availability#36121

Merged
peppy merged 3 commits intoppy:masterfrom
smoogipoo:mp-fix-availability-tracker
Dec 24, 2025
Merged

Fix potentially incorrect online play beatmap availability#36121
peppy merged 3 commits intoppy:masterfrom
smoogipoo:mp-fix-availability-tracker

Conversation

@smoogipoo
Copy link
Copy Markdown
Contributor

@smoogipoo smoogipoo commented Dec 24, 2025

Has been discussed several times already. The condition added in this PR is the same as used in multiplayer, playlists, DC, and QP.

I believe this to be one of the contributors to #36045. Some background knowledge is necessary:

  • The multiplayer server sends LoadRequested to all users in the room regardless of their beatmap availability. This is an issue on its own that will be fixed separately.
  • Additionally to the above, there is no guard in the client that, upon receiving LoadRequested, blocks gameplay from being entered if not in the correct (WaitingForLoad or Spectating) state.
  • Quick play sends both a Ready state and beatmap availability to the server BUT only relies on the ready state to decide when a match can be started.
  • So if the ready state and beatmap availability get out of sync and critically one user does not have the beatmap available, then quick play will force-start the match, send LoadRequested to all users, but only keep track of those that have the beatmap available for gameplay purposes.

tl;dr: It's all messed up. Iteratively returning to sanity.

@smoogipoo smoogipoo force-pushed the mp-fix-availability-tracker branch from 56f79ed to ecaf3e0 Compare December 24, 2025 07:53
@peppy peppy self-requested a review December 24, 2025 09:12
@peppy
Copy link
Copy Markdown
Member

peppy commented Dec 24, 2025

It's getting quite a struggle to track all these locally copy-pasted cases. How about something like this?

diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 62c03b4fcd..d50862a369 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -328,6 +328,14 @@ public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
              .Filter(query, arguments)
              .FirstOrDefault()?.Detach());
 
+        /// <summary>
+        /// Perform a lookup query on available <see cref="BeatmapInfo"/>s for a specific online ID.
+        /// </summary>
+        /// <returns>A matching local beatmap info if existing and in a valid state.</returns>
+        public BeatmapInfo? QueryOnlineBeatmapId(int id) => Realm.Run(r =>
+            r.All<BeatmapInfo>()
+             .ForOnlineId(id).SingleOrDefault()?.Detach());
+
         /// <summary>
         /// A default representation of a WorkingBeatmap to use when no beatmap is available.
         /// </summary>
diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs
index 1bb6b0aba4..65ae42a3da 100644
--- a/osu.Game/Database/RealmExtensions.cs
+++ b/osu.Game/Database/RealmExtensions.cs
@@ -2,7 +2,9 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Linq;
 using osu.Framework.Logging;
+using osu.Game.Beatmaps;
 using Realms;
 
 namespace osu.Game.Database
@@ -102,5 +104,13 @@ public static T Write<T>(this Realm realm, Func<Realm, T> function)
         /// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback.
         /// </remarks>
         public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0;
+
+        public static IQueryable<BeatmapInfo> NotDeleted(this IQueryable<BeatmapInfo> beatmaps) =>
+            beatmaps.Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false");
+
+        public static IQueryable<BeatmapInfo> ForOnlineId(this IQueryable<BeatmapInfo> beatmaps, int id) =>
+            beatmaps
+                .NotDeleted()
+                .Filter($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", id);
     }
 }
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
index c3648a7edf..6db293ec71 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
@@ -489,7 +489,7 @@ public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager
             if (!screen.IsCurrentScreen())
                 return;
 
-            var beatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.Beatmap.OnlineID);
+            var beatmap = beatmaps.QueryOnlineBeatmapId(item.Beatmap.OnlineID);
 
             screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally.
             screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID);
diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs
index a689ac85ee..d692783e48 100644
--- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs
+++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs
@@ -246,7 +246,7 @@ private void updateGameplayState()
 
             // Update global gameplay state to correspond to the new selection.
             // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
-            var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
+            var localBeatmap = beatmapManager.QueryOnlineBeatmapId(item.BeatmapID);
 
             if (localBeatmap != null)
             {
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 16c6a46a9c..05fd5b8c69 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -510,7 +510,7 @@ private void onActivePlaylistItemChanged()
             {
                 MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
 
-                var newBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
+                var newBeatmap = beatmapManager.QueryOnlineBeatmapId(item.BeatmapID);
 
                 if (!Beatmap.Value.BeatmapSetInfo.Equals(newBeatmap?.BeatmapSet))
                     this.MakeCurrent();
@@ -652,7 +652,7 @@ private void updateGameplayState()
 
             // Update global gameplay state to correspond to the new selection.
             // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
-            var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmapId);
+            var localBeatmap = beatmapManager.QueryOnlineBeatmapId(gameplayBeatmapId);
             Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
             Ruleset.Value = ruleset;
             Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray();
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayBeatmapAvailabilityTracker.cs
index d4d161a3d0..1bb67cc3af 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayBeatmapAvailabilityTracker.cs
@@ -17,7 +17,6 @@
 using osu.Game.Online;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Online.Rooms;
-using Realms;
 
 namespace osu.Game.Screens.OnlinePlay
 {
@@ -153,13 +152,9 @@ void updateAvailability()
                 }
             }
 
-            IQueryable<BeatmapInfo> queryBeatmap()
-            {
-                // See: BeatmapManager.QueryBeatmap()
-                return realm.Realm.All<BeatmapInfo>()
-                            .Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false")
-                            .Filter($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmap.OnlineID);
-            }
+            IQueryable<BeatmapInfo> queryBeatmap() =>
+                realm.Realm.All<BeatmapInfo>()
+                     .ForOnlineId(beatmap.OnlineID);
         }
 
         protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index e994299606..df5a9db8e7 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -62,8 +62,7 @@ protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem
         [BackgroundDependencyLoader]
         private void load()
         {
-            var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}",
-                PlaylistItem.Beatmap.OnlineID);
+            var localBeatmap = beatmapManager.QueryOnlineBeatmapId(PlaylistItem.Beatmap.OnlineID);
             itemBeatmap = beatmapManager.GetWorkingBeatmap(localBeatmap);
 
             AddInternal(new Container
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
index fdda6f6c85..58e260c857 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
@@ -609,7 +609,7 @@ private void updateGameplayState()
 
             // Update global gameplay state to correspond to the new selection.
             // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
-            var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID);
+            var localBeatmap = beatmapManager.QueryOnlineBeatmapId(gameplayBeatmap.OnlineID);
             Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
             Ruleset.Value = gameplayRuleset;
             Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray();

Copy link
Copy Markdown
Member

@peppy peppy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as commented

Co-authored-by: Dean Herbert <pe@ppy.sh>
@pull-request-size pull-request-size bot added size/M and removed size/S labels Dec 24, 2025
@peppy peppy merged commit 2d06c2b into ppy:master Dec 24, 2025
7 of 9 checks passed
@peppy peppy deleted the mp-fix-availability-tracker branch December 24, 2025 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants