From f3524ad8f545efe40f63374a7b1a6a2b26220db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Mar 2025 15:00:34 +0100 Subject: [PATCH 1/5] Fix daily challenge not querying beatmap properly Noticed during review of https://github.com/ppy/osu/pull/32571. The reproduction scenario is as follows: 1. Download beatmap used in daily challenge 2. Go to editor and modify it 3. Go to daily challenge, wherein the availability tracker will notice the MD5 mismatch, block the button, and require a redownload 4. Redownload the beatmap 5. Start gameplay 6. Gameplay start will fail due to web not issuing a submission token because the attempt to start gameplay ended up using the modified version of the map from step (2) rather than the redownloaded original from step (4). Thankfully, due to (6), this is not exploitable, but nevertheless pretty bad. Probably regressed somewhere around https://github.com/ppy/osu/pull/31747 actually. --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index cb881ccfe5..43db586c45 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -490,7 +490,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (!screen.IsCurrentScreen()) return; - var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); + var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID && b.MD5Hash == item.Beatmap.MD5Hash); screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID); From f73601535d4a1a1a39bb989a11f942a7e46bd6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Mar 2025 09:33:35 +0100 Subject: [PATCH 2/5] Use alternative method of fixing issue of improper beatmap query in daily challenge (and expand to other online play modes) --- osu.Game/Beatmaps/BeatmapManager.cs | 16 +++++++++++++++- .../OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Playlists/PlaylistsRoomSubScreen.cs | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 1e66b28b15..2c17908487 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -298,7 +298,21 @@ namespace osu.Game.Beatmaps /// The query. /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => - r.All().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); + r.All().Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); + + /// + /// Perform a lookup query on available s. + /// Use this overload instead of + /// when Realm is unable to transform an expression to the internal Realm query syntax. + /// + /// The query. + /// The arguments for the query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo? QueryBeatmap(string query, params QueryArgument[] arguments) => Realm.Run(r => + r.All() + .Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false") + .Filter(query, arguments) + .FirstOrDefault()?.Detach()); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 43db586c45..171524870f 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -490,7 +490,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (!screen.IsCurrentScreen()) return; - var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID && b.MD5Hash == item.Beatmap.MD5Hash); + var beatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", 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/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index eca59c8393..39cdaaf2e9 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -456,7 +456,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 89416e66bf..6aa93ca2ba 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -598,7 +598,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // 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 int beatmapId = gameplayBeatmap.OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); From 094a22a4a0de146575849db1a2320d0998a1c38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Mar 2025 09:46:14 +0100 Subject: [PATCH 3/5] Use `==` consistently --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 171524870f..893bc4eb5c 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -490,7 +490,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge 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.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", 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/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 39cdaaf2e9..6c8043d8bb 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -456,7 +456,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 6aa93ca2ba..b13a418276 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -598,7 +598,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // 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 int beatmapId = gameplayBeatmap.OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); From 1f77ef554443298ad0bfdd6c8753211f6df5de09 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 17:53:45 +0900 Subject: [PATCH 4/5] Resolve inspection --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index b13a418276..1fefdb350c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -597,7 +597,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // 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 - int beatmapId = gameplayBeatmap.OnlineID; var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; From 2ba621550c840aa9e92c917338b744b381df6752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Mar 2025 12:39:19 +0100 Subject: [PATCH 5/5] Fix tests --- .../Visual/Multiplayer/QueueModeTestScene.cs | 5 +++++ .../Visual/Multiplayer/TestSceneMultiplayer.cs | 5 +++++ .../Playlists/TestScenePlaylistsRoomCreation.cs | 6 ++++++ .../Playlists/TestScenePlaylistsRoomSubScreen.cs | 12 ++++++++---- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 0e01751d76..0e8093f459 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -61,6 +61,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + Realm.Write(r => + { + foreach (var beatmapInfo in r.All()) + beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; + }); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); InitialBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); OtherBeatmap = importedSet.Beatmaps.Last(b => b.Ruleset.OnlineID == 0); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index ae939c7b9e..a62d9676c2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -81,6 +81,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + Realm.Write(r => + { + foreach (var beatmapInfo in r.All()) + beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; + }); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index a748d61d44..2e90f08d47 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -176,6 +176,7 @@ namespace osu.Game.Tests.Visual.Playlists RulesetID = new OsuRuleset().RulesetInfo.OnlineID } ]; + room.EndDate = DateTimeOffset.Now.AddHours(1); }); AddAssert("match has default beatmap", () => match.Beatmap.IsDefault); @@ -212,6 +213,11 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach(); + Realm.Write(r => + { + foreach (var beatmapInfo in r.All()) + beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; + }); }); private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index e9c4b56e04..1841e2fd52 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -65,7 +65,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 1, DifficultyName = "Osu 1", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "11111111", + OnlineMD5Hash = "11111111", Ruleset = new OsuRuleset().RulesetInfo, Metadata = { @@ -79,7 +80,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 2, DifficultyName = "Osu 2", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "22222222", + OnlineMD5Hash = "22222222", Ruleset = new OsuRuleset().RulesetInfo, Metadata = { @@ -93,7 +95,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 3, DifficultyName = "Taiko 1", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "33333333", + OnlineMD5Hash = "33333333", Ruleset = new TaikoRuleset().RulesetInfo, Metadata = { @@ -107,7 +110,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 4, DifficultyName = "Taiko 2", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "44444444", + OnlineMD5Hash = "44444444", Ruleset = new TaikoRuleset().RulesetInfo, Metadata = { diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 1f0c08714e..caf99a4cf6 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -68,6 +68,7 @@ namespace osu.Game.Tests.Beatmaps var b = Decoder.GetDecoder(reader).Decode(reader); b.BeatmapInfo.MD5Hash = test_beatmap_hash.Value.md5; + b.BeatmapInfo.OnlineMD5Hash = test_beatmap_hash.Value.md5; b.BeatmapInfo.Hash = test_beatmap_hash.Value.sha2; return b;