1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-04 12:24:42 +08:00
Files
osu-lazer/osu.Game/Screens/Select/PanelBeatmapStandalone.cs
T
Bartłomiej Dach 8050ee36ce Add support for grouping by keys in song select for osu!mania (#37285)
https://github.com/user-attachments/assets/edfc8d06-4f04-4876-84a5-dfc83a18f160

Of note:

- Supports both native beatmaps and converts
- Supports key mods (changing key mods will trigger song select refilter
when key count grouping is engaged)
- The option to group by keys is only visible when mania ruleset is
active
- If the user selects key count grouping and then switches to another
ruleset, song select will fall back to no grouping, but this change will
not be written back to config. Only the user changing the grouping mode
manually will reflect in config changes. This is done so that key
grouping persists across ruleset changes, and this even survives game
restarts.

---

I've only done some light behaviour testing on this because this feature
needs a lot of subjective shot calls and I don't want to commit too deep
before I get a temperature check on the shot calls I made here.

In particular some performance profiling of
https://github.com/ppy/osu/commit/7de8f70b1dbbdf2e3f13ba10faf25329abf6468d
may be warranted.
2026-04-15 19:01:20 +09:00

336 lines
14 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Screens.Select
{
public partial class PanelBeatmapStandalone : Panel
{
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
private IBindable<StarDifficulty>? starDifficultyBindable;
private CancellationTokenSource? starDifficultyCancellationSource;
private PanelSetBackground beatmapBackground = null!;
private ScheduledDelegate? scheduledBackgroundRetrieval;
private OsuSpriteText titleText = null!;
private OsuSpriteText artistText = null!;
private PanelUpdateBeatmapButton updateButton = null!;
private BeatmapSetOnlineStatusPill statusPill = null!;
private ConstrainedIconContainer difficultyIcon = null!;
private StarRatingDisplay starRatingDisplay = null!;
private SpreadDisplay spreadDisplay = null!;
private PanelLocalRankDisplay localRank = null!;
private OsuSpriteText keyCountText = null!;
private OsuSpriteText difficultyText = null!;
private OsuSpriteText authorText = null!;
private FillFlowContainer mainFill = null!;
private Box backgroundBorder = null!;
private BeatmapInfo beatmap => ((GroupedBeatmap)Item!.Model).Beatmap;
public PanelBeatmapStandalone()
{
PanelXOffset = 20;
}
[BackgroundDependencyLoader]
private void load()
{
Height = HEIGHT;
Icon = difficultyIcon = new ConstrainedIconContainer
{
Size = new Vector2(12),
Margin = new MarginPadding { Left = 4f, Right = 3f },
Colour = colourProvider.Background5,
};
Background = backgroundBorder = new Box
{
RelativeSizeAxes = Axes.Both,
};
Content.Children = new Drawable[]
{
beatmapBackground = new PanelSetBackground(),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Spacing = new Vector2(5),
Margin = new MarginPadding { Left = 6.5f },
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
localRank = new PanelLocalRankDisplay
{
Scale = new Vector2(0.8f),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
mainFill = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Bottom = 4.8f },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
titleText = new OsuSpriteText
{
Font = OsuFont.Style.Heading2.With(typeface: Typeface.TorusAlternate, weight: FontWeight.Bold),
},
artistText = new OsuSpriteText
{
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
Padding = new MarginPadding { Top = -2 },
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 2, Bottom = 2 },
Children = new Drawable[]
{
statusPill = new BeatmapSetOnlineStatusPill
{
Animated = false,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
TextSize = OsuFont.Style.Caption2.Size,
Margin = new MarginPadding { Right = 4f },
},
updateButton = new PanelUpdateBeatmapButton
{
Scale = new Vector2(0.8f),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Right = 4f, Bottom = -1f },
},
keyCountText = new OsuSpriteText
{
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Alpha = 0,
},
difficultyText = new OsuSpriteText
{
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Right = 3f },
},
authorText = new OsuSpriteText
{
Colour = colourProvider.Content2,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft
}
}
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
Spacing = new Vector2(3),
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.875f),
},
spreadDisplay = new SpreadDisplay
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
}
},
}
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ruleset.BindValueChanged(_ => updateKeyCount());
mods.BindValueChanged(_ => updateKeyCount(), true);
Selected.BindValueChanged(s =>
{
Expanded.Value = s.NewValue;
spreadDisplay.Enabled.Value = s.NewValue;
}, true);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
var beatmapSet = beatmap.BeatmapSet!;
scheduledBackgroundRetrieval = Scheduler.AddDelayed(b => beatmapBackground.Beatmap = beatmaps.GetWorkingBeatmap(b), beatmap, 50);
titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title);
artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist);
updateButton.BeatmapSet = beatmapSet;
statusPill.Status = beatmap.Status;
difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon();
difficultyIcon.Show();
localRank.Beatmap = beatmap;
difficultyText.Text = beatmap.DifficultyName;
authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username);
computeStarRating();
spreadDisplay.Beatmap.Value = beatmap;
updateKeyCount();
}
protected override void FreeAfterUse()
{
base.FreeAfterUse();
scheduledBackgroundRetrieval?.Cancel();
scheduledBackgroundRetrieval = null;
beatmapBackground.Beatmap = null;
updateButton.BeatmapSet = null;
localRank.Beatmap = null;
starDifficultyBindable = null;
spreadDisplay.Beatmap.Value = null;
starDifficultyCancellationSource?.Cancel();
}
private void computeStarRating()
{
starDifficultyCancellationSource?.Cancel();
starDifficultyCancellationSource = new CancellationTokenSource();
if (Item == null)
return;
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
starDifficultyBindable.BindValueChanged(starDifficulty =>
{
starRatingDisplay.Current.Value = starDifficulty.NewValue;
spreadDisplay.StarDifficulty.Value = starDifficulty.NewValue;
}, true);
}
protected override void Update()
{
base.Update();
if (Item?.IsVisible != true)
{
starDifficultyCancellationSource?.Cancel();
starDifficultyCancellationSource = null;
}
// Dirty hack to make sure we don't take up spacing in parent fill flow when not displaying a rank.
// I can't find a better way to do this.
mainFill.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
var diffColour = starRatingDisplay.DisplayedDifficultyColour;
AccentColour = diffColour;
spreadDisplay.Current.Colour = diffColour;
backgroundBorder.Colour = diffColour;
difficultyIcon.Colour = starRatingDisplay.DisplayedDifficultyTextColour;
}
private void updateKeyCount()
{
if (Item == null)
return;
var rulesetInstance = ruleset.Value.CreateInstance();
if (rulesetInstance.AvailableVariants.Count() > 1)
{
int variant = rulesetInstance.GetVariantForBeatmap(beatmap, mods.Value);
var variantName = rulesetInstance.GetVariantName(variant);
keyCountText.Alpha = 1;
keyCountText.Text = LocalisableString.Interpolate($"[{variantName}] ");
}
else
keyCountText.Alpha = 0;
}
public override MenuItem[] ContextMenuItems
{
get
{
if (Item == null)
return Array.Empty<MenuItem>();
List<MenuItem> items = new List<MenuItem>();
if (songSelect != null)
items.AddRange(songSelect.GetForwardActions(beatmap));
return items.ToArray();
}
}
}
}