From 0bbad3e1cd4a9fba9435ef18f45060f0d4f72ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 11:31:21 +0200 Subject: [PATCH 1/3] Extract helper method for retrieving all user local scores --- osu.Game/Scoring/ScoreInfoExtensions.cs | 14 ++++++++++++++ .../Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++------ osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 10 +++------- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 ++------ osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs | 10 +++------- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index dd08326742..33b880a794 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Models; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select.Leaderboards; +using Realms; namespace osu.Game.Scoring { @@ -64,5 +66,17 @@ namespace osu.Game.Scoring /// The to compute the maximum achievable combo for. /// The maximum achievable combo. public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); + + /// + /// Performs a realm filter that returns all scores that belong to the user with the given . + /// (for guests) is supported. + /// + public static IQueryable GetAllLocalScoresForUser(this Realm realm, int? userId) + { + return realm.All() + .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $@" && {nameof(ScoreInfo.DeletePending)} == false", userId); + } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 3c1aec745d..5c5c814c5b 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -19,7 +19,6 @@ using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Scoring; @@ -246,11 +245,8 @@ namespace osu.Game.Screens.Ranking.Statistics // We may want to iterate on the following conditions further in the future var localUserScore = AchievedScore ?? realm.Run(r => - r.All() - .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $@" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, newScore.BeatmapInfo.ID, newScore.BeatmapInfo.Ruleset.ShortName) + r.GetAllLocalScoresForUser(api.LocalUser.Value.Id) + .Filter($@"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0", newScore.BeatmapInfo.ID) .AsEnumerable() .OrderByDescending(score => score.Ruleset.MatchesOnlineID(newScore.BeatmapInfo.Ruleset)) .ThenByDescending(score => score.Rank) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index da9661f702..6f1f2e8370 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -59,12 +58,9 @@ namespace osu.Game.Screens.Select.Carousel { scoreSubscription?.Dispose(); scoreSubscription = realm.RegisterForNotifications(r => - r.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName), + r.GetAllLocalScoresForUser(api.LocalUser.Value.Id) + .Filter($@"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1", beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); }, true); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c2711ceef0..04cad06745 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -25,7 +25,6 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; -using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select; @@ -693,11 +692,8 @@ namespace osu.Game.Screens.SelectV2 { var topRankMapping = new Dictionary(); - var allLocalScores = r.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" - + $" && {nameof(ScoreInfo.DeletePending)} == false", criteria.LocalUserId, criteria.Ruleset?.ShortName) + var allLocalScores = r.GetAllLocalScoresForUser(criteria.LocalUserId) + .Filter($@"{nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0", criteria.Ruleset?.ShortName) .OrderByDescending(s => s.TotalScore) .ThenBy(s => s.Date); diff --git a/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs index 273f995794..c72835144f 100644 --- a/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs +++ b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -78,12 +77,9 @@ namespace osu.Game.Screens.SelectV2 return; scoreSubscription = realm.RegisterForNotifications(r => - r.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmap.ID, ruleset.Value.ShortName), + r.GetAllLocalScoresForUser(api.LocalUser.Value.Id) + .Filter($@"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1", beatmap.ID, ruleset.Value.ShortName), localScoresChanged); } From 15d73ce07edcbfeb45e484b9a4f500b69da17138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 11:57:24 +0200 Subject: [PATCH 2/3] Add test coverage --- .../SongSelect/TestSceneTopLocalRank.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 79baae53e8..93b9efed6a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select.Carousel; @@ -161,5 +163,53 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("SS rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.X); } + + [Test] + public void TestGuestScore() + { + AddStep("Add score for guest user", () => + { + var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap); + + testScoreInfo.User = new GuestUser(); + testScoreInfo.Rank = ScoreRank.B; + + scoreManager.Import(testScoreInfo); + }); + + AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank, () => Is.EqualTo(ScoreRank.B)); + } + + [Test] + public void TestUnknownUserScore() + { + AddStep("Add score for unknown user", () => + { + var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap); + + testScoreInfo.User = new APIUser { Username = "AAA", }; + testScoreInfo.Rank = ScoreRank.S; + + scoreManager.Import(testScoreInfo); + }); + + AddUntilStep("S rank displayed", () => topLocalRank.DisplayedRank, () => Is.EqualTo(ScoreRank.S)); + } + + [Test] + public void TestAnotherUserScore() + { + AddStep("Add score for not-current user", () => + { + var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap); + + testScoreInfo.User = new APIUser { Username = "notme", Id = 43, }; + testScoreInfo.Rank = ScoreRank.S; + + scoreManager.Import(testScoreInfo); + }); + + AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank, () => Is.Null); + } } } From f1a020d2c6392cc0f98f0a9b8da716987a44a303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Sep 2025 11:37:08 +0200 Subject: [PATCH 3/3] Treat guest user scores & scores of unknown users as the local user's --- osu.Game/Scoring/ScoreInfoExtensions.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 33b880a794..2eec0399d6 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Models; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select.Leaderboards; using Realms; @@ -71,10 +72,16 @@ namespace osu.Game.Scoring /// Performs a realm filter that returns all scores that belong to the user with the given . /// (for guests) is supported. /// + /// + /// All guest scores (with user ID of ), + /// as well as scores of unknown provenance (with default user ID of 1, see ), + /// will be treated as if they belong to the local user. + /// This may not be necessarily considered fully correct in some circumstances, but in most cases it is the desired effect. + /// public static IQueryable GetAllLocalScoresForUser(this Realm realm, int? userId) { return realm.All() - .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + .Filter($@"({nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0 || {nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} <= 1)" + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + $@" && {nameof(ScoreInfo.DeletePending)} == false", userId); }