diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 822e5f26bd..5dc6f950a5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -99,8 +99,35 @@ namespace osu.Game.Tests.Visual.Online Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), }, + TopTags = + [ + new APIBeatmapTag { TagId = 4, VoteCount = 1 }, + new APIBeatmapTag { TagId = 2, VoteCount = 1 }, + new APIBeatmapTag { TagId = 23, VoteCount = 5 }, + ], }, }, + RelatedTags = + [ + new APITag + { + Id = 2, + Name = "song representation/simple", + Description = "Accessible and straightforward map design." + }, + new APITag + { + Id = 4, + Name = "style/clean", + Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects." + }, + new APITag + { + Id = 23, + Name = "aim/aim control", + Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern." + } + ] }); }); diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index d98715a42d..c6cf0f735f 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -128,6 +128,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"converts")] public APIBeatmap[]? Converts { get; set; } + [JsonProperty(@"related_tags")] + public APITag[]? RelatedTags { get; set; } + private BeatmapMetadata metadata => new BeatmapMetadata { Title = Title, diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index d21b2546b9..37741b63ce 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -9,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapListing; @@ -17,26 +20,22 @@ namespace osu.Game.Overlays.BeatmapSet { public partial class Info : Container { - private const float metadata_width = 175; + private const float metadata_width = 185; private const float spacing = 20; - private const float base_height = 220; + private const float base_height = 300; private readonly Box successRateBackground; private readonly Box background; - private readonly SuccessRate successRate; + private readonly MetadataSection userTags; public readonly Bindable BeatmapSet = new Bindable(); - - public APIBeatmap? BeatmapInfo - { - get => successRate.Beatmap; - set => successRate.Beatmap = value; - } + public readonly Bindable Beatmap = new Bindable(); public Info() { + SuccessRate successRate; MetadataSectionNominators nominators; - MetadataSection source, tags; + MetadataSection source, mapperTags; MetadataSectionGenre genre; MetadataSectionLanguage language; OsuSpriteText notRankedPlaceholder; @@ -66,27 +65,30 @@ namespace osu.Game.Overlays.BeatmapSet Child = new MetadataSectionDescription(), }, }, - new Container + new OsuScrollContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = metadata_width, - Padding = new MarginPadding { Horizontal = 10 }, + Padding = new MarginPadding { Left = 10 }, Margin = new MarginPadding { Right = BeatmapSetOverlay.RIGHT_WIDTH + spacing }, Masking = true, + ScrollbarOverlapsContent = false, Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { nominators = new MetadataSectionNominators(), source = new MetadataSectionSource(), genre = new MetadataSectionGenre { Width = 0.5f }, language = new MetadataSectionLanguage { Width = 0.5f }, - tags = new MetadataSectionTags(), + userTags = new MetadataSectionUserTags(), + mapperTags = new MetadataSectionMapperTags(), }, }, }, @@ -121,18 +123,42 @@ namespace osu.Game.Overlays.BeatmapSet }, }; - BeatmapSet.ValueChanged += b => + BeatmapSet.BindValueChanged(b => { nominators.Metadata = (b.NewValue?.CurrentNominations ?? Array.Empty(), b.NewValue?.RelatedUsers ?? Array.Empty()); source.Metadata = b.NewValue?.Source ?? string.Empty; - tags.Metadata = b.NewValue?.Tags ?? string.Empty; + mapperTags.Metadata = b.NewValue?.Tags ?? string.Empty; + updateUserTags(); genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified }; language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified }; bool setHasLeaderboard = b.NewValue?.Status > 0; successRate.Alpha = setHasLeaderboard ? 1 : 0; notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1; - Height = setHasLeaderboard ? 270 : base_height; - }; + }); + Beatmap.BindValueChanged(b => + { + successRate.Beatmap = b.NewValue; + updateUserTags(); + }); + } + + private void updateUserTags() + { + if (Beatmap.Value?.TopTags == null || Beatmap.Value.TopTags.Length == 0 || BeatmapSet.Value?.RelatedTags == null) + { + userTags.Metadata = null; + return; + } + + var tagsById = BeatmapSet.Value.RelatedTags.ToDictionary(t => t.Id); + userTags.Metadata = Beatmap.Value.TopTags + .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) + .Where(t => t.relatedTag != null) + // 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) + .Select(t => t.relatedTag!.Name) + .ToArray(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionMapperTags.cs similarity index 80% rename from osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs rename to osu.Game/Overlays/BeatmapSet/MetadataSectionMapperTags.cs index fc16ba19d8..47e839a84d 100644 --- a/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs +++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionMapperTags.cs @@ -7,10 +7,10 @@ using osu.Game.Online.Chat; namespace osu.Game.Overlays.BeatmapSet { - public partial class MetadataSectionTags : MetadataSection + public partial class MetadataSectionMapperTags : MetadataSection { - public MetadataSectionTags(Action? searchAction = null) - : base(MetadataType.Tags, searchAction) + public MetadataSectionMapperTags(Action? searchAction = null) + : base(MetadataType.MapperTags, searchAction) { } diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionUserTags.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionUserTags.cs new file mode 100644 index 0000000000..3a9fe8d33f --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionUserTags.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.BeatmapSet +{ + public partial class MetadataSectionUserTags : MetadataSection + { + private readonly Action? searchAction; + + public MetadataSectionUserTags(Action? searchAction = null) + : base(MetadataType.UserTags, null) + { + this.searchAction = searchAction; + } + + protected override void AddMetadata(string[]? tags, LinkFlowContainer loaded) + { + if (tags == null) + return; + + for (int i = 0; i <= tags.Length - 1; i++) + { + string tag = tags[i]; + + if (searchAction != null) + loaded.AddLink(tag, () => searchAction(tag)); + else + loaded.AddLink(tag, LinkAction.SearchBeatmapSet, $@"tag=""""{tag}"""""); + + if (i != tags.Length - 1) + loaded.AddText(" "); + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/MetadataType.cs b/osu.Game/Overlays/BeatmapSet/MetadataType.cs index c92cecc17e..dba6a63679 100644 --- a/osu.Game/Overlays/BeatmapSet/MetadataType.cs +++ b/osu.Game/Overlays/BeatmapSet/MetadataType.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; @@ -8,8 +9,11 @@ namespace osu.Game.Overlays.BeatmapSet { public enum MetadataType { - [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoTags))] - Tags, + [Description("User Tags")] // TODO: use translated string after osu-resources update + UserTags, + + [Description("Mapper Tags")] // TODO: use translated string after osu-resources update + MapperTags, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))] Source, diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 8de21129d3..255e30038b 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -47,7 +47,10 @@ namespace osu.Game.Overlays Spacing = new Vector2(0, 20), Children = new Drawable[] { - info = new Info(), + info = new Info + { + Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap } + }, new ScoresContainer { Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap } @@ -60,11 +63,7 @@ namespace osu.Game.Overlays info.BeatmapSet.BindTo(beatmapSet); comments.BeatmapSet.BindTo(beatmapSet); - Header.HeaderContent.Picker.Beatmap.ValueChanged += b => - { - info.BeatmapInfo = b.NewValue; - ScrollFlow.ScrollToStart(); - }; + Header.HeaderContent.Picker.Beatmap.ValueChanged += b => ScrollFlow.ScrollToStart(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 2bb60716ff..6a6a4cddf3 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Select { description = new MetadataSectionDescription(query => songSelect?.Search(query)), source = new MetadataSectionSource(query => songSelect?.Search(query)), - tags = new MetadataSectionTags(query => songSelect?.Search(query)), + tags = new MetadataSectionMapperTags(query => songSelect?.Search(query)), }, }, },