diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs new file mode 100644 index 0000000000..8a674d43a5 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -0,0 +1,161 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.SongSelect; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapTitleWedge : SongSelectComponentsTestScene + { + private RulesetStore rulesets = null!; + + private BeatmapTitleWedge titleWedge = null!; + private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType().Single(); + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + this.rulesets = rulesets; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddRange(new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + titleWedge = new BeatmapTitleWedge + { + State = { Value = Visibility.Visible }, + }, + }, + } + }); + + AddSliderStep("change star difficulty", 0, 11.9, 4.18, v => + { + ((BindableDouble)difficultyDisplay.DisplayedStars).Value = v; + }); + } + + [Test] + public void TestNullBeatmap() + { + selectBeatmap(null); + AddAssert("check default title", () => titleWedge.DisplayedTitle == Beatmap.Default.BeatmapInfo.Metadata.Title); + AddAssert("check default artist", () => titleWedge.DisplayedArtist == Beatmap.Default.BeatmapInfo.Metadata.Artist); + AddAssert("check empty version", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedVersion.ToString())); + AddAssert("check empty author", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedAuthor.ToString())); + AddAssert("check no statistics", () => difficultyDisplay.ChildrenOfType().All(d => !d.Statistics.Any())); + } + + [Test] + public void TestBPMUpdates() + { + const double bpm = 120; + IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); + + OsuModDoubleTime doubleTime = null!; + + selectBeatmap(beatmap); + checkDisplayedBPM($"{bpm}"); + + AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); + checkDisplayedBPM($"{bpm * 1.5f}"); + + AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2); + checkDisplayedBPM($"{bpm * 2}"); + + AddStep("select HT", () => SelectedMods.Value = new[] { new OsuModHalfTime() }); + checkDisplayedBPM($"{bpm * 0.75f}"); + } + + [Test] + public void TestRulesetChange() + { + selectBeatmap(Beatmap.Value.Beatmap); + + AddWaitStep("wait for select", 3); + + foreach (var rulesetInfo in rulesets.AvailableRulesets) + { + var testBeatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(rulesetInfo); + + setRuleset(rulesetInfo); + selectBeatmap(testBeatmap); + } + } + + [Test] + public void TestWedgeVisibility() + { + AddStep("hide", () => { titleWedge.Hide(); }); + AddWaitStep("wait for hide", 3); + AddAssert("check visibility", () => titleWedge.Alpha == 0); + AddStep("show", () => { titleWedge.Show(); }); + AddWaitStep("wait for show", 1); + AddAssert("check visibility", () => titleWedge.Alpha > 0); + } + + [TestCase(120, 125, null, "120-125 (mostly 120)")] + [TestCase(120, 120.6, null, "120-121 (mostly 120)")] + [TestCase(120, 120.4, null, "120")] + [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] + [TestCase(120, 120.4, "DT", "180")] + public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) + { + IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm }); + beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + + if (mod != null) + AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) }); + + selectBeatmap(beatmap); + checkDisplayedBPM(expectedDisplay); + } + + private void setRuleset(RulesetInfo rulesetInfo) + { + AddStep("set ruleset", () => Ruleset.Value = rulesetInfo); + } + + private void selectBeatmap(IBeatmap? b) + { + AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => + { + Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); + }); + } + + private void checkDisplayedBPM(string target) + { + AddUntilStep($"displayed bpm is {target}", () => + { + var label = titleWedge.ChildrenOfType().Single(l => l.TooltipText == BeatmapsetsStrings.ShowStatsBpm); + return label.Value == target; + }); + } + } +} diff --git a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs index 957ee23e3b..bdb10a477c 100644 --- a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs +++ b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs @@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Mods { public partial class AdjustedAttributesTooltip : VisibilityContainer, ITooltip { + private readonly OverlayColourProvider? colourProvider; private FillFlowContainer attributesFillFlow = null!; private Container content = null!; @@ -27,6 +28,11 @@ namespace osu.Game.Overlays.Mods [Resolved] private OsuColour colours { get; set; } = null!; + public AdjustedAttributesTooltip(OverlayColourProvider? colourProvider = null) + { + this.colourProvider = colourProvider; + } + [BackgroundDependencyLoader] private void load() { @@ -45,7 +51,7 @@ namespace osu.Game.Overlays.Mods new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Gray3, + Colour = colourProvider?.Background4 ?? colours.Gray3, }, new FillFlowContainer { diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs new file mode 100644 index 0000000000..9d1be2fc37 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -0,0 +1,324 @@ +// 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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge : VisibilityContainer + { + private const float corner_radius = 10; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + private ModSettingChangeTracker? settingChangeTracker; + + private BeatmapSetOnlineStatusPill statusPill = null!; + private Container titleContainer = null!; + private OsuHoverContainer titleLink = null!; + private OsuSpriteText titleLabel = null!; + private Container artistContainer = null!; + private OsuHoverContainer artistLink = null!; + private OsuSpriteText artistLabel = null!; + + internal string DisplayedTitle => titleLabel.Text.ToString(); + internal string DisplayedArtist => artistLabel.Text.ToString(); + + private StatisticPlayCount playCount = null!; + private Statistic favouritesStatistic = null!; + private Statistic lengthStatistic = null!; + private Statistic bpmStatistic = null!; + + [Resolved] + private SongSelect? songSelect { get; set; } + + [Resolved] + private LocalisationManager localisation { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private APIBeatmapSet? currentOnlineBeatmapSet; + private GetBeatmapSetRequest? currentRequest; + + public BeatmapTitleWedge() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + Shear = OsuGame.SHEAR; + Masking = true; + CornerRadius = corner_radius; + + InternalChildren = new Drawable[] + { + new WedgeBackground(), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding + { + Top = SongSelect.WEDGE_CONTENT_MARGIN, + Left = SongSelect.WEDGE_CONTENT_MARGIN + }, + Spacing = new Vector2(0f, 4f), + Children = new Drawable[] + { + new ShearAligningWrapper(statusPill = new BeatmapSetOnlineStatusPill + { + Shear = -OsuGame.SHEAR, + ShowUnknownStatus = true, + TextSize = OsuFont.Style.Caption1.Size, + TextPadding = new MarginPadding { Horizontal = 6, Vertical = 1 }, + }), + new ShearAligningWrapper(titleContainer = new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + Height = OsuFont.Style.Title.Size, + Margin = new MarginPadding { Bottom = -4f }, + Child = titleLink = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Child = titleLabel = new TruncatingSpriteText + { + Shadow = true, + Font = OsuFont.Style.Title, + }, + } + }), + new ShearAligningWrapper(artistContainer = new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + Height = OsuFont.Style.Heading2.Size, + Margin = new MarginPadding { Left = 1f }, + Child = artistLink = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Child = artistLabel = new TruncatingSpriteText + { + Shadow = true, + Font = OsuFont.Style.Heading2, + }, + } + }), + new ShearAligningWrapper(new FillFlowContainer + { + Shear = -OsuGame.SHEAR, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f, 0f), + AutoSizeDuration = 100, + AutoSizeEasing = Easing.OutQuint, + Children = new Drawable[] + { + playCount = new StatisticPlayCount(background: true, leftPadding: SongSelect.WEDGE_CONTENT_MARGIN, minSize: 50f) + { + Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN }, + }, + favouritesStatistic = new Statistic(OsuIcon.Heart, background: true, minSize: 25f) + { + TooltipText = BeatmapsStrings.StatusFavourites, + }, + lengthStatistic = new Statistic(OsuIcon.Clock), + bpmStatistic = new Statistic(OsuIcon.Metronome) + { + TooltipText = BeatmapsetsStrings.ShowStatsBpm, + Margin = new MarginPadding { Left = 5f }, + }, + }, + }), + new ShearAligningWrapper(new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN }, + Padding = new MarginPadding { Right = -SongSelect.WEDGE_CONTENT_MARGIN }, + Child = new DifficultyDisplay(), + }), + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmap.BindValueChanged(_ => updateDisplay()); + ruleset.BindValueChanged(_ => updateDisplay()); + + mods.BindValueChanged(m => + { + settingChangeTracker?.Dispose(); + + updateLengthAndBpmStatistics(); + + settingChangeTracker = new ModSettingChangeTracker(m.NewValue); + settingChangeTracker.SettingChanged += _ => updateLengthAndBpmStatistics(); + }); + + updateDisplay(); + + FinishTransforms(true); + } + + protected override void PopIn() + { + this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + protected override void PopOut() + { + this.MoveToX(-150, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + protected override void Update() + { + base.Update(); + titleLabel.MaxWidth = titleContainer.DrawWidth - 20; + artistLabel.MaxWidth = artistContainer.DrawWidth - 20; + } + + private void updateDisplay() + { + var metadata = beatmap.Value.Metadata; + var beatmapInfo = beatmap.Value.BeatmapInfo; + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + statusPill.Status = beatmapInfo.Status; + + var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title); + titleLabel.Text = titleText; + titleLink.Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)); + + var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + artistLabel.Text = artistText; + artistLink.Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)); + + updateLengthAndBpmStatistics(); + + if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID) + refetchBeatmapSet(); + + updateOnlineDisplay(); + } + + private void updateLengthAndBpmStatistics() + { + var beatmapInfo = beatmap.Value.BeatmapInfo; + + double rate = ModUtils.CalculateRateWithMods(mods.Value); + + int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); + int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); + int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); + + double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); + double hitLength = Math.Round(beatmapInfo.Length / rate); + + lengthStatistic.Value = hitLength.ToFormattedDuration(); + lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); + + bpmStatistic.Value = bpmMin == bpmMax + ? $"{bpmMin}" + : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; + } + + private void refetchBeatmapSet() + { + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + currentRequest?.Cancel(); + currentRequest = null; + currentOnlineBeatmapSet = null; + + if (beatmapSetInfo.OnlineID >= 1) + { + // todo: consider introducing a BeatmapSetLookupCache for caching benefits. + currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID); + currentRequest.Failure += _ => updateOnlineDisplay(); + currentRequest.Success += s => + { + currentOnlineBeatmapSet = s; + updateOnlineDisplay(); + }; + + api.Queue(currentRequest); + } + } + + private void updateOnlineDisplay() + { + if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) + { + playCount.Value = null; + favouritesStatistic.Value = null; + } + else if (currentOnlineBeatmapSet == null) + { + playCount.Value = new StatisticPlayCount.Data(-1, -1); + favouritesStatistic.Value = "-"; + } + else + { + var onlineBeatmapSet = currentOnlineBeatmapSet; + var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID); + + if (onlineBeatmap != null) + { + playCount.FadeIn(300, Easing.OutQuint); + playCount.Value = new StatisticPlayCount.Data(onlineBeatmap.PlayCount, onlineBeatmap.UserPlayCount); + } + else + { + playCount.FadeOut(300, Easing.OutQuint); + playCount.Value = null; + } + + favouritesStatistic.FadeIn(300, Easing.OutQuint); + favouritesStatistic.Value = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0"); + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs new file mode 100644 index 0000000000..e8b2ccb04a --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -0,0 +1,392 @@ +// 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 System.Collections.Generic; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Online; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge + { + public partial class DifficultyDisplay : CompositeDrawable + { + private const float border_weight = 2; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + private ModSettingChangeTracker? settingChangeTracker; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private StarRatingDisplay starRatingDisplay = null!; + private FillFlowContainer nameLine = null!; + private OsuSpriteText difficultyText = null!; + private OsuSpriteText mappedByText = null!; + private OsuHoverContainer mapperLink = null!; + private OsuSpriteText mapperText = null!; + + internal LocalisableString DisplayedVersion => difficultyText.Text; + internal LocalisableString DisplayedAuthor => mapperText.Text; + + private GridContainer ratingAndNameContainer = null!; + private DifficultyStatisticsDisplay countStatisticsDisplay = null!; + private AdjustableDifficultyStatisticsDisplay difficultyStatisticsDisplay = null!; + + private CancellationTokenSource? cancellationSource; + + public IBindable DisplayedStars => displayedStars; + + private readonly Bindable displayedStars = new BindableDouble(); + + public DifficultyDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Masking = true; + CornerRadius = 10; + Shear = OsuGame.SHEAR; + + InternalChildren = new Drawable[] + { + new WedgeBackground(), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new ShearAligningWrapper(ratingAndNameContainer = new GridContainer + { + Shear = -OsuGame.SHEAR, + AlwaysPresent = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Vertical = 5f }, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 6), + new Dimension(), + }, + Content = new[] + { + new[] + { + starRatingDisplay = new StarRatingDisplay(default, animated: true) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + Empty(), + nameLine = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Bottom = 2f }, + Children = new Drawable[] + { + difficultyText = new TruncatingSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), + }, + mappedByText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = " mapped by ", + Font = OsuFont.Style.Body, + }, + mapperLink = new MapperLinkContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Child = mapperText = new TruncatingSpriteText + { + Shadow = true, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), + }, + }, + }, + }, + } + }, + }), + new ShearAligningWrapper(new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Bottom = border_weight, Right = border_weight }, + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + CornerRadius = 10 - border_weight, + Shear = OsuGame.SHEAR, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5.Opacity(0.8f), + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 20f, Vertical = 7.5f }, + Shear = -OsuGame.SHEAR, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + countStatisticsDisplay = new DifficultyStatisticsDisplay + { + RelativeSizeAxes = Axes.X, + }, + Empty(), + difficultyStatisticsDisplay = new AdjustableDifficultyStatisticsDisplay(autoSize: true), + } + }, + } + }, + } + }), + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmap.BindValueChanged(_ => updateDisplay()); + ruleset.BindValueChanged(_ => updateDisplay()); + + mods.BindValueChanged(m => + { + settingChangeTracker?.Dispose(); + + updateDifficultyStatistics(); + + settingChangeTracker = new ModSettingChangeTracker(m.NewValue); + settingChangeTracker.SettingChanged += _ => updateDifficultyStatistics(); + }); + + updateDisplay(); + + displayedStars.BindValueChanged(_ => updateStars(), true); + FinishTransforms(true); + } + + [Resolved] + private ILinkHandler? linkHandler { get; set; } + + private void updateDisplay() + { + cancellationSource?.Cancel(); + cancellationSource = new CancellationTokenSource(); + + computeStarDifficulty(cancellationSource.Token); + + if (beatmap.IsDefault) + { + ratingAndNameContainer.FadeOut(300, Easing.OutQuint); + difficultyText.Text = string.Empty; + mapperText.Text = string.Empty; + countStatisticsDisplay.Statistics = Array.Empty(); + } + else + { + ratingAndNameContainer.FadeIn(300, Easing.OutQuint); + difficultyText.Text = beatmap.Value.BeatmapInfo.DifficultyName; + mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, beatmap.Value.Metadata.Author)); + mapperText.Text = beatmap.Value.Metadata.Author.Username; + + var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); + + countStatisticsDisplay.Statistics = playableBeatmap.GetStatistics() + .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content)) + .ToList(); + } + + updateDifficultyStatistics(); + } + + private void updateDifficultyStatistics() => Scheduler.AddOnce(() => + { + if (beatmap.IsDefault) + { + difficultyStatisticsDisplay.TooltipContent = null; + difficultyStatisticsDisplay.Statistics = Array.Empty(); + return; + } + + BeatmapDifficulty baseDifficulty = beatmap.Value.BeatmapInfo.Difficulty; + BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty); + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(originalDifficulty); + + var rateAdjustedDifficulty = originalDifficulty; + + if (ruleset.Value != null) + { + double rate = ModUtils.CalculateRateWithMods(mods.Value); + + rateAdjustedDifficulty = ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + difficultyStatisticsDisplay.TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, rateAdjustedDifficulty); + } + + StatisticDifficulty.Data firstStatistic; + + switch (ruleset.Value?.OnlineID) + { + case 3: + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + + // For the time being, the key count is static no matter what, because: + // a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering. + // b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion. + int keyCount = legacyRuleset.GetKeyCount(beatmap.Value.BeatmapInfo, mods.Value); + + firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCsMania, keyCount, keyCount, 10); + break; + + default: + firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCs, baseDifficulty.CircleSize, rateAdjustedDifficulty.CircleSize, 10); + break; + } + + difficultyStatisticsDisplay.Statistics = new[] + { + firstStatistic, + new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAccuracy, baseDifficulty.OverallDifficulty, rateAdjustedDifficulty.OverallDifficulty, 10), + new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsDrain, baseDifficulty.DrainRate, rateAdjustedDifficulty.DrainRate, 10), + new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAr, baseDifficulty.ApproachRate, rateAdjustedDifficulty.ApproachRate, 10), + }; + }); + + private void updateStars() + { + starRatingDisplay.Current.Value = new StarDifficulty(displayedStars.Value, 0); + + Color4 colour = displayedStars.Value >= 6.5f ? colours.Orange1 : colours.ForStarDifficulty(displayedStars.Value); + difficultyText.FadeColour(colour, 300, Easing.OutQuint); + mappedByText.FadeColour(colour, 300, Easing.OutQuint); + countStatisticsDisplay.TransformTo(nameof(countStatisticsDisplay.AccentColour), colour, 300, Easing.OutQuint); + difficultyStatisticsDisplay.TransformTo(nameof(difficultyStatisticsDisplay.AccentColour), colour, 300, Easing.OutQuint); + } + + private void computeStarDifficulty(CancellationToken cancellationToken) + { + difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken) + .ContinueWith(task => + { + Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + var result = task.GetResultSafely() ?? default; + displayedStars.Value = result.Stars; + }); + }, cancellationToken); + } + + protected override void Update() + { + base.Update(); + difficultyText.MaxWidth = Math.Max(nameLine.DrawWidth - mappedByText.DrawWidth - mapperText.DrawWidth - 20, 0); + } + + private partial class MapperLinkContainer : OsuHoverContainer + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) + { + TooltipText = ContextMenuStrings.ViewProfile; + IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + } + } + + private partial class AdjustableDifficultyStatisticsDisplay : DifficultyStatisticsDisplay, IHasCustomTooltip + { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ITooltip GetCustomTooltip() => new AdjustedAttributesTooltip(colourProvider); + + public AdjustedAttributesTooltip.Data? TooltipContent { get; set; } + + public AdjustableDifficultyStatisticsDisplay(bool autoSize) + : base(autoSize) + { + } + } + } + } +}