osukey/osu.Game/Screens/Select/BeatmapInfoWedge.cs
Salman Ahmed b419ea716b Refactor beatmap info wedge to not fully refresh on star difficulty change
Makes it look awkward when changing difficulty via mod settings for
example.

Now the changes should instead only affect the displayed components which consume it
directly.
2021-08-17 05:46:06 +03:00

536 lines
21 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Select
{
public class BeatmapInfoWedge : VisibilityContainer
{
public const float BORDER_THICKNESS = 2.5f;
private const float shear_width = 36.75f;
private const float transition_duration = 250;
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
protected Container DisplayedContent { get; private set; }
protected WedgeInfoText Info { get; private set; }
public BeatmapInfoWedge()
{
Shear = wedged_container_shear;
Masking = true;
BorderColour = new Color4(221, 255, 255, 255);
BorderThickness = BORDER_THICKNESS;
Alpha = 0;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = new Color4(130, 204, 255, 150),
Radius = 20,
Roundness = 15,
};
}
[BackgroundDependencyLoader]
private void load()
{
ruleset.BindValueChanged(_ => updateDisplay());
}
protected override void PopIn()
{
this.MoveToX(0, 800, Easing.OutQuint);
this.RotateTo(0, 800, Easing.OutQuint);
this.FadeIn(transition_duration);
}
protected override void PopOut()
{
this.MoveToX(-100, 800, Easing.In);
this.RotateTo(10, 800, Easing.In);
this.FadeOut(transition_duration * 2, Easing.In);
}
private WorkingBeatmap beatmap;
public WorkingBeatmap Beatmap
{
get => beatmap;
set
{
if (beatmap == value) return;
beatmap = value;
updateDisplay();
}
}
public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback
private Container loadingInfo;
private void updateDisplay()
{
Scheduler.AddOnce(perform);
void perform()
{
void removeOldInfo()
{
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
DisplayedContent?.FadeOut(transition_duration);
DisplayedContent?.Expire();
DisplayedContent = null;
}
if (beatmap == null)
{
removeOldInfo();
return;
}
LoadComponentAsync(loadingInfo = new Container
{
RelativeSizeAxes = Axes.Both,
Shear = -Shear,
Depth = DisplayedContent?.Depth + 1 ?? 0,
Children = new Drawable[]
{
new BeatmapInfoWedgeBackground(beatmap),
Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value),
}
}, loaded =>
{
// ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return;
removeOldInfo();
Add(DisplayedContent = loaded);
});
}
}
public class WedgeInfoText : Container
{
public OsuSpriteText VersionLabel { get; private set; }
public OsuSpriteText TitleLabel { get; private set; }
public OsuSpriteText ArtistLabel { get; private set; }
public BeatmapSetOnlineStatusPill StatusPill { get; private set; }
public FillFlowContainer MapperContainer { get; private set; }
private DifficultyColourBar difficultyColourBar;
private StarRatingDisplay starRatingDisplay;
private ILocalisedBindableString titleBinding;
private ILocalisedBindableString artistBinding;
private FillFlowContainer infoLabelContainer;
private Container bpmLabelContainer;
private readonly WorkingBeatmap beatmap;
private readonly RulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods;
private ModSettingChangeTracker settingChangeTracker;
public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods)
{
this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
this.mods = mods;
}
private CancellationTokenSource cancellationSource;
private IBindable<StarDifficulty?> starDifficulty;
[BackgroundDependencyLoader]
private void load(LocalisationManager localisation, BeatmapDifficultyCache difficultyCache)
{
var beatmapInfo = beatmap.BeatmapInfo;
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
RelativeSizeAxes = Axes.Both;
titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist));
Children = new Drawable[]
{
difficultyColourBar = new DifficultyColourBar
{
RelativeSizeAxes = Axes.Y,
Width = 20,
},
new FillFlowContainer
{
Name = "Topleft-aligned metadata",
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Top = 10, Left = 25, Right = shear_width * 2.5f },
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
VersionLabel = new OsuSpriteText
{
Text = beatmapInfo.Version,
Font = OsuFont.GetFont(size: 24, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
}
},
new FillFlowContainer
{
Name = "Topright-aligned metadata",
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Top = 14, Right = shear_width / 2 },
AutoSizeAxes = Axes.Both,
Shear = wedged_container_shear,
Children = new Drawable[]
{
starRatingDisplay = new StarRatingDisplay(default)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Shear = -wedged_container_shear,
Alpha = 0f,
},
StatusPill = new BeatmapSetOnlineStatusPill
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Shear = -wedged_container_shear,
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
Status = beatmapInfo.Status,
}
}
},
new FillFlowContainer
{
Name = "Centre-aligned metadata",
Anchor = Anchor.CentreLeft,
Origin = Anchor.TopLeft,
Y = -7,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Left = 25, Right = shear_width },
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
TitleLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 28, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
ArtistLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 17, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
MapperContainer = new FillFlowContainer
{
Margin = new MarginPadding { Top = 10 },
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Child = getMapper(metadata),
},
infoLabelContainer = new FillFlowContainer
{
Margin = new MarginPadding { Top = 20 },
Spacing = new Vector2(20, 0),
AutoSizeAxes = Axes.Both,
}
}
}
};
titleBinding.BindValueChanged(_ => setMetadata(metadata.Source));
artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true);
starDifficulty = difficultyCache.GetBindableDifficulty(beatmapInfo, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
difficultyColourBar.Current.Value = s.NewValue ?? default;
starRatingDisplay.FadeIn(transition_duration);
starRatingDisplay.Current.Value = s.NewValue ?? default;
});
// no difficulty means it can't have a status to show
if (beatmapInfo.Version == null)
StatusPill.Hide();
addInfoLabels();
}
private void setMetadata(string source)
{
ArtistLabel.Text = artistBinding.Value;
TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
}
private void addInfoLabels()
{
if (beatmap.Beatmap?.HitObjects?.Any() != true)
return;
infoLabelContainer.Children = new Drawable[]
{
new InfoLabel(new BeatmapStatistic
{
Name = "Length",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
Content = beatmap.BeatmapInfo.Length.ToFormattedDuration().ToString(),
}),
bpmLabelContainer = new Container
{
AutoSizeAxes = Axes.Both,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(20, 0),
Children = getRulesetInfoLabels()
}
};
settingChangeTracker = new ModSettingChangeTracker(mods);
settingChangeTracker.SettingChanged += _ => refreshBPMLabel();
refreshBPMLabel();
}
private InfoLabel[] getRulesetInfoLabels()
{
try
{
IBeatmap playableBeatmap;
try
{
// Try to get the beatmap with the user's ruleset
playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
}
catch (BeatmapInvalidForRulesetException)
{
// Can't be converted to the user's ruleset, so use the beatmap's own ruleset
playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>());
}
return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray();
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap successfully!");
}
return Array.Empty<InfoLabel>();
}
private void refreshBPMLabel()
{
var b = beatmap.Beatmap;
if (b == null)
return;
// this doesn't consider mods which apply variable rates, yet.
double rate = 1;
foreach (var mod in mods.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate);
double bpmMax = b.ControlPointInfo.BPMMaximum * rate;
double bpmMin = b.ControlPointInfo.BPMMinimum * rate;
double mostCommonBPM = 60000 / b.GetMostCommonBeatLength() * rate;
string labelText = Precision.AlmostEquals(bpmMin, bpmMax)
? $"{bpmMin:0}"
: $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})";
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
{
Name = "BPM",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
Content = labelText
});
}
private Drawable getMapper(BeatmapMetadata metadata)
{
if (metadata.Author == null)
return Empty();
return new LinkFlowContainer(s =>
{
s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.AddText("mapped by ");
d.AddUserLink(metadata.Author);
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
settingChangeTracker?.Dispose();
cancellationSource?.Cancel();
}
public class InfoLabel : Container, IHasTooltip
{
public LocalisableString TooltipText { get; }
public InfoLabel(BeatmapStatistic statistic)
{
TooltipText = statistic.Name;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(20),
Children = new[]
{
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"441288"),
Icon = FontAwesome.Solid.Square,
Rotation = 45,
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"f7dd55"),
Icon = FontAwesome.Regular.Circle,
Size = new Vector2(0.8f)
},
statistic.CreateIcon().With(i =>
{
i.Anchor = Anchor.Centre;
i.Origin = Anchor.Centre;
i.RelativeSizeAxes = Axes.Both;
i.Colour = Color4Extensions.FromHex(@"f7dd55");
i.Size = new Vector2(0.64f);
}),
}
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = new Color4(255, 221, 85, 255),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Margin = new MarginPadding { Left = 30 },
Text = statistic.Content,
}
};
}
}
public class DifficultyColourBar : Container, IHasCurrentValue<StarDifficulty>
{
private readonly BindableWithCurrent<StarDifficulty> current = new BindableWithCurrent<StarDifficulty>();
public Bindable<StarDifficulty> Current
{
get => current.Current;
set => current.Current = value;
}
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
const float full_opacity_ratio = 0.7f;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Width = full_opacity_ratio,
},
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Alpha = 0.5f,
X = full_opacity_ratio,
Width = 1 - full_opacity_ratio,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(c =>
{
this.FadeColour(colours.ForStarDifficulty(c.NewValue.Stars), transition_duration, Easing.OutQuint);
}, true);
FinishTransforms(true);
}
}
}
}
}