From e64e0f0ca9c1de81ecef8effb92781d4bd8f7df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Mar 2026 12:41:40 +0100 Subject: [PATCH 1/6] Fix beatmap set overlay not showing user tags at all Regressed in 60d9c358b8ec6a4be778078956c907a4d47c0360. In general an `APIBeatmap`'s `BeatmapSet` need not be present. In the usage site of `osu.Game.Overlays.BeatmapSet.Info`, `Beatmap` and `BeatmapSet` were actually two separate bindables. Moving the logic to a helper, and therefore implicitly moving `BeatmapSet` from tracking said separate bindable to instead refer to `Beatmap.BeatmapSet` broke this. --- .../BeatmapSet/BeatmapSetHeaderContent.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index f75e7b1d3c..3c5f15adde 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,12 +242,14 @@ namespace osu.Game.Overlays.BeatmapSet BeatmapSet.BindValueChanged(setInfo => { - Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; - cover.OnlineInfo = setInfo.NewValue; + var newBeatmapSet = setInfo.NewValue; + + Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = newBeatmapSet; + cover.OnlineInfo = newBeatmapSet; downloadTracker?.RemoveAndDisposeImmediately(); - if (setInfo.NewValue == null) + if (newBeatmapSet == null) { onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); videoIconPill.Hide(); @@ -261,7 +263,10 @@ namespace osu.Game.Overlays.BeatmapSet } else { - downloadTracker = new BeatmapDownloadTracker(setInfo.NewValue); + foreach (var beatmap in newBeatmapSet.Beatmaps) + beatmap.BeatmapSet = newBeatmapSet; + + downloadTracker = new BeatmapDownloadTracker(newBeatmapSet); downloadTracker.State.BindValueChanged(_ => updateDownloadButtons()); AddInternal(downloadTracker); @@ -269,18 +274,18 @@ namespace osu.Game.Overlays.BeatmapSet loading.Hide(); - if (setInfo.NewValue.HasVideo) + if (newBeatmapSet.HasVideo) videoIconPill.Show(); else videoIconPill.Hide(); - if (setInfo.NewValue.HasStoryboard) + if (newBeatmapSet.HasStoryboard) storyboardIconPill.Show(); else storyboardIconPill.Hide(); - var titleText = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title); - var artistText = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist); + var titleText = new RomanisableString(newBeatmapSet.TitleUnicode, newBeatmapSet.Title); + var artistText = new RomanisableString(newBeatmapSet.ArtistUnicode, newBeatmapSet.Artist); title.Clear(); artist.Clear(); @@ -290,13 +295,13 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); - if (setInfo.NewValue.HasExplicitContent) + if (newBeatmapSet.HasExplicitContent) { title.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); title.AddArbitraryDrawable(new ExplicitContentBeatmapBadge()); } - if (setInfo.NewValue.FeaturedInSpotlight) + if (newBeatmapSet.FeaturedInSpotlight) { title.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); @@ -304,7 +309,7 @@ namespace osu.Game.Overlays.BeatmapSet artist.AddLink(artistText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"artist=""""{artistText}""""")); - if (setInfo.NewValue.TrackId != null) + if (newBeatmapSet.TrackId != null) { artist.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); artist.AddArbitraryDrawable(new FeaturedArtistBeatmapBadge()); From 2f96e96576e12115a3ea43b9ac62b1c2000f16a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Mar 2026 12:48:00 +0100 Subject: [PATCH 2/6] Migrate realm populating online lookup source to use helper method for retrieving tags To reduce logic duplication. --- .../Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index 2b75820587..821e791140 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -101,10 +101,9 @@ namespace osu.Game.Screens.SelectV2 if (dbBeatmap.MatchesOnlineVersion && dbBeatmap.Status != onlineBeatmap.Status) dbBeatmap.Status = onlineBeatmap.Status; - HashSet userTags = onlineBeatmap.TopTags? - .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) - .Where(t => t.relatedTag != null) - .Select(t => t.relatedTag!.Name) + onlineBeatmap.BeatmapSet = onlineBeatmapSet; + HashSet userTags = onlineBeatmap.GetTopUserTags()? + .Select(t => t.Tag.Name) .ToHashSet() ?? []; if (!userTags.SetEquals(dbBeatmap.Metadata.UserTags)) From 65b49137ee8ee9f233e6b9cc4bde589bd28bf235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Mar 2026 12:49:41 +0100 Subject: [PATCH 3/6] Move constant closer to helper --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 6 ++++++ .../Screens/Ranking/UserTagControl.DrawableUserTag.cs | 9 ++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 3a0afcd0ab..ed2a395fc9 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -117,6 +117,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"owners")] public BeatmapOwner[] BeatmapOwners { get; set; } = Array.Empty(); + /// + /// Minimum count of votes required to display a tag on the beatmap's page. + /// Should match value specified web-side as https://github.com/ppy/osu-web/blob/cae2fdf03cfb8c30c8e332cfb142e03188ceffef/config/osu.php#L59. + /// + public const int MINIMUM_USER_TAG_VOTES_FOR_DISPLAY = 5; + public (APITag Tag, int VoteCount)[] GetTopUserTags() { if (TopTags == null || TopTags.Length == 0 || BeatmapSet?.RelatedTags == null) diff --git a/osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs b/osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs index 09af68a4f7..2a26ebc3c6 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Screens.Ranking { @@ -20,12 +21,6 @@ namespace osu.Game.Screens.Ranking { public partial class DrawableUserTag : OsuAnimatedButton { - /// - /// Minimum count of votes required to display a tag on the beatmap's page. - /// Should match value specified web-side as https://github.com/ppy/osu-web/blob/cae2fdf03cfb8c30c8e332cfb142e03188ceffef/config/osu.php#L59. - /// - public const int MIN_VOTES_DISPLAY = 5; - public readonly UserTag UserTag; public Action? OnSelected { get; set; } @@ -160,7 +155,7 @@ namespace osu.Game.Screens.Ranking { voteCount.BindValueChanged(_ => { - confirmed.Value = voteCount.Value >= MIN_VOTES_DISPLAY; + confirmed.Value = voteCount.Value >= APIBeatmap.MINIMUM_USER_TAG_VOTES_FOR_DISPLAY; }, true); voted.BindValueChanged(v => { From d9e182230d085984e4650cfd3298d7812f664a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Mar 2026 12:55:14 +0100 Subject: [PATCH 4/6] Add bool flag for checking tag vote threshold & utilise as required Closes https://github.com/ppy/osu/issues/36453. My omission was in assuming that web was going to start filtering out the tags below the threshold from API responses, which is not the case. Whether or not the consumers want or not to display tags below threshold is subjective. I figured that the matchmaking card tooltip might want to display below threshold but I dunno. --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 12 ++++++++++-- osu.Game/Overlays/BeatmapSet/Info.cs | 2 +- .../MatchmakingSelectPanel.CardContentBeatmap.cs | 4 ++-- .../SelectV2/RealmPopulatingOnlineLookupSource.cs | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ed2a395fc9..0d3724a769 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -123,7 +123,15 @@ namespace osu.Game.Online.API.Requests.Responses /// public const int MINIMUM_USER_TAG_VOTES_FOR_DISPLAY = 5; - public (APITag Tag, int VoteCount)[] GetTopUserTags() + /// + /// Retrieves top user tags for the beatmap, ordered in a way matching osu!web. + /// Requires to be populated. + /// + /// + /// If , only tags above will be shown. + /// If , all tags regardless of vote count will be shown. + /// + public (APITag Tag, int VoteCount)[] GetTopUserTags(bool confirmedOnly) { if (TopTags == null || TopTags.Length == 0 || BeatmapSet?.RelatedTags == null) return []; @@ -132,7 +140,7 @@ namespace osu.Game.Online.API.Requests.Responses return TopTags .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) - .Where(t => t.relatedTag != null) + .Where(t => t.relatedTag != null && (!confirmedOnly || t.topTag.VoteCount >= MINIMUM_USER_TAG_VOTES_FOR_DISPLAY)) // see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria .OrderByDescending(t => t.topTag.VoteCount) .ThenBy(t => t.relatedTag!.Name) diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index e25c1370c5..ca29724e83 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.BeatmapSet private void updateUserTags() { - userTags.Metadata = Beatmap.Value?.GetTopUserTags().Select(t => t.Tag.Name).ToArray(); + userTags.Metadata = Beatmap.Value?.GetTopUserTags(confirmedOnly: true).Select(t => t.Tag.Name).ToArray(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs index e2d5fa7890..de27e8fc62 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs @@ -458,7 +458,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect new OsuSpriteText { Padding = new MarginPadding { Vertical = 3, Horizontal = 8 }, - Text = beatmap.GetTopUserTags().FirstOrDefault().Tag?.Name ?? string.Empty, + Text = beatmap.GetTopUserTags(confirmedOnly: true).FirstOrDefault().Tag?.Name ?? string.Empty, AlwaysPresent = true, Colour = colourProvider.Content2, Font = OsuFont.Style.Caption2, @@ -468,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect }; } - public LocalisableString TooltipText => string.Join('\n', beatmap.GetTopUserTags().Select(t => $"{t.Tag.Name} ({t.VoteCount})")); + public LocalisableString TooltipText => string.Join('\n', beatmap.GetTopUserTags(confirmedOnly: false).Select(t => $"{t.Tag.Name} ({t.VoteCount})")); } } } diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index 821e791140..46f97345c1 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -102,7 +102,7 @@ namespace osu.Game.Screens.SelectV2 dbBeatmap.Status = onlineBeatmap.Status; onlineBeatmap.BeatmapSet = onlineBeatmapSet; - HashSet userTags = onlineBeatmap.GetTopUserTags()? + HashSet userTags = onlineBeatmap.GetTopUserTags(confirmedOnly: true) .Select(t => t.Tag.Name) .ToHashSet() ?? []; From 1aa42a73b102e7d5d452c2953bf367ba4530696b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Mar 2026 13:38:23 +0100 Subject: [PATCH 5/6] Fix code quality --- osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index 46f97345c1..1850e6f6ac 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -70,7 +70,6 @@ namespace osu.Game.Screens.SelectV2 private static void updateRealmBeatmapSet(Realm r, APIBeatmapSet onlineBeatmapSet) { - var tagsById = (onlineBeatmapSet.RelatedTags ?? []).ToDictionary(t => t.Id); var onlineBeatmaps = onlineBeatmapSet.Beatmaps.ToDictionary(b => b.OnlineID); var dbBeatmapSets = r.All().Where(b => b.OnlineID == onlineBeatmapSet.OnlineID); @@ -104,7 +103,7 @@ namespace osu.Game.Screens.SelectV2 onlineBeatmap.BeatmapSet = onlineBeatmapSet; HashSet userTags = onlineBeatmap.GetTopUserTags(confirmedOnly: true) .Select(t => t.Tag.Name) - .ToHashSet() ?? []; + .ToHashSet(); if (!userTags.SetEquals(dbBeatmap.Metadata.UserTags)) { From 9b55e1dfdb5836d4be1b93f8947919a4ff610baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2026 10:34:35 +0100 Subject: [PATCH 6/6] Only show user tags above threshold by default --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 2 +- osu.Game/Overlays/BeatmapSet/Info.cs | 2 +- .../MatchmakingSelectPanel.CardContentBeatmap.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 0d3724a769..9c79927363 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -131,7 +131,7 @@ namespace osu.Game.Online.API.Requests.Responses /// If , only tags above will be shown. /// If , all tags regardless of vote count will be shown. /// - public (APITag Tag, int VoteCount)[] GetTopUserTags(bool confirmedOnly) + public (APITag Tag, int VoteCount)[] GetTopUserTags(bool confirmedOnly = true) { if (TopTags == null || TopTags.Length == 0 || BeatmapSet?.RelatedTags == null) return []; diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index ca29724e83..e25c1370c5 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.BeatmapSet private void updateUserTags() { - userTags.Metadata = Beatmap.Value?.GetTopUserTags(confirmedOnly: true).Select(t => t.Tag.Name).ToArray(); + userTags.Metadata = Beatmap.Value?.GetTopUserTags().Select(t => t.Tag.Name).ToArray(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs index de27e8fc62..e2d5fa7890 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs @@ -458,7 +458,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect new OsuSpriteText { Padding = new MarginPadding { Vertical = 3, Horizontal = 8 }, - Text = beatmap.GetTopUserTags(confirmedOnly: true).FirstOrDefault().Tag?.Name ?? string.Empty, + Text = beatmap.GetTopUserTags().FirstOrDefault().Tag?.Name ?? string.Empty, AlwaysPresent = true, Colour = colourProvider.Content2, Font = OsuFont.Style.Caption2, @@ -468,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect }; } - public LocalisableString TooltipText => string.Join('\n', beatmap.GetTopUserTags(confirmedOnly: false).Select(t => $"{t.Tag.Name} ({t.VoteCount})")); + public LocalisableString TooltipText => string.Join('\n', beatmap.GetTopUserTags().Select(t => $"{t.Tag.Name} ({t.VoteCount})")); } } }