mirror of
https://github.com/ppy/osu.git
synced 2026-05-22 04:09:54 +08:00
Add grouping support for most modes
This commit is contained in:
@@ -56,21 +56,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)"))
|
||||
Item = new CarouselItem(new GroupDefinition(0, $"{star} Star(s)", new StarDifficulty(star, 0)))
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")),
|
||||
Item = new CarouselItem(new GroupDefinition(1, $"{star} Star(s)", new StarDifficulty(star, 0))),
|
||||
KeyboardSelected = { Value = true },
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")),
|
||||
Item = new CarouselItem(new GroupDefinition(2, $"{star} Star(s)", new StarDifficulty(star, 0))),
|
||||
Expanded = { Value = true },
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")),
|
||||
Item = new CarouselItem(new GroupDefinition(3, $"{star} Star(s)", new StarDifficulty(star, 0))),
|
||||
Expanded = { Value = true },
|
||||
KeyboardSelected = { Value = true },
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace osu.Game.Beatmaps
|
||||
/// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))]
|
||||
[Description("Local")]
|
||||
LocallyModified = -4,
|
||||
|
||||
[Description("Unknown")]
|
||||
|
||||
@@ -423,5 +423,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record GroupDefinition(object Data, string Title);
|
||||
/// <summary>
|
||||
/// Defines a grouping header for a set of carousel items.
|
||||
/// </summary>
|
||||
/// <param name="Order">The order of this group in the carousel, sorted using ascending order.</param>
|
||||
/// <param name="Title">The title of this group.</param>
|
||||
/// <param name="Data">Additional data. Provide a <see cref="StarDifficulty"/> for difficulty groups, or null for any other group.</param>
|
||||
public record GroupDefinition(int Order, string Title, object? Data = null);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select;
|
||||
@@ -19,15 +21,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// <summary>
|
||||
/// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection.
|
||||
/// </summary>
|
||||
public IDictionary<BeatmapSetInfo, HashSet<CarouselItem>> SetItems => setItems;
|
||||
public IDictionary<BeatmapSetInfo, HashSet<CarouselItem>> SetItems => setMap;
|
||||
|
||||
/// <summary>
|
||||
/// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection.
|
||||
/// </summary>
|
||||
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupItems;
|
||||
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupMap;
|
||||
|
||||
private readonly Dictionary<BeatmapSetInfo, HashSet<CarouselItem>> setItems = new Dictionary<BeatmapSetInfo, HashSet<CarouselItem>>();
|
||||
private readonly Dictionary<GroupDefinition, HashSet<CarouselItem>> groupItems = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
|
||||
private readonly Dictionary<BeatmapSetInfo, HashSet<CarouselItem>> setMap = new Dictionary<BeatmapSetInfo, HashSet<CarouselItem>>();
|
||||
private readonly Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
|
||||
|
||||
private readonly Func<FilterCriteria> getCriteria;
|
||||
|
||||
@@ -40,72 +42,70 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
setItems.Clear();
|
||||
groupItems.Clear();
|
||||
setMap.Clear();
|
||||
groupMap.Clear();
|
||||
|
||||
var criteria = getCriteria();
|
||||
var newItems = new List<CarouselItem>();
|
||||
|
||||
BeatmapInfo? lastBeatmap = null;
|
||||
|
||||
GroupDefinition? lastGroup = null;
|
||||
CarouselItem? lastGroupItem = null;
|
||||
|
||||
HashSet<CarouselItem>? currentGroupItems = null;
|
||||
HashSet<CarouselItem>? currentSetItems = null;
|
||||
|
||||
BeatmapSetsGroupedTogether = criteria.Sort != SortMode.Difficulty;
|
||||
|
||||
foreach (var item in items)
|
||||
var groups = getGroups((List<CarouselItem>)items, criteria);
|
||||
|
||||
foreach (var (group, itemsInGroup) in groups)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var beatmap = (BeatmapInfo)item.Model;
|
||||
CarouselItem? groupItem = null;
|
||||
HashSet<CarouselItem>? currentGroupItems = null;
|
||||
HashSet<CarouselItem>? currentSetItems = null;
|
||||
BeatmapInfo? lastBeatmap = null;
|
||||
|
||||
if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup)
|
||||
if (group != null)
|
||||
{
|
||||
// When reaching a new group, ensure we reset any beatmap set tracking.
|
||||
currentSetItems = null;
|
||||
lastBeatmap = null;
|
||||
groupMap[group] = currentGroupItems = new HashSet<CarouselItem>();
|
||||
|
||||
groupItems[newGroup] = currentGroupItems = new HashSet<CarouselItem>();
|
||||
lastGroup = newGroup;
|
||||
|
||||
addItem(lastGroupItem = new CarouselItem(newGroup)
|
||||
addItem(groupItem = new CarouselItem(group)
|
||||
{
|
||||
DrawHeight = PanelGroup.HEIGHT,
|
||||
DepthLayer = -2,
|
||||
});
|
||||
}
|
||||
|
||||
if (BeatmapSetsGroupedTogether)
|
||||
foreach (var item in itemsInGroup)
|
||||
{
|
||||
bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID;
|
||||
var beatmap = (BeatmapInfo)item.Model;
|
||||
|
||||
if (newBeatmapSet)
|
||||
if (BeatmapSetsGroupedTogether)
|
||||
{
|
||||
setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>();
|
||||
bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID;
|
||||
|
||||
if (lastGroupItem != null)
|
||||
lastGroupItem.NestedItemCount++;
|
||||
|
||||
addItem(new CarouselItem(beatmap.BeatmapSet!)
|
||||
if (newBeatmapSet)
|
||||
{
|
||||
DrawHeight = PanelBeatmapSet.HEIGHT,
|
||||
DepthLayer = -1
|
||||
});
|
||||
if (!setMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems))
|
||||
setMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>();
|
||||
|
||||
if (groupItem != null)
|
||||
groupItem.NestedItemCount++;
|
||||
|
||||
addItem(new CarouselItem(beatmap.BeatmapSet!)
|
||||
{
|
||||
DrawHeight = PanelBeatmapSet.HEIGHT,
|
||||
DepthLayer = -1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastGroupItem != null)
|
||||
lastGroupItem.NestedItemCount++;
|
||||
else
|
||||
{
|
||||
if (groupItem != null)
|
||||
groupItem.NestedItemCount++;
|
||||
|
||||
item.DrawHeight = PanelBeatmapStandalone.HEIGHT;
|
||||
}
|
||||
item.DrawHeight = PanelBeatmapStandalone.HEIGHT;
|
||||
}
|
||||
|
||||
addItem(item);
|
||||
lastBeatmap = beatmap;
|
||||
addItem(item);
|
||||
lastBeatmap = beatmap;
|
||||
}
|
||||
|
||||
void addItem(CarouselItem i)
|
||||
{
|
||||
@@ -114,7 +114,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
currentGroupItems?.Add(i);
|
||||
currentSetItems?.Add(i);
|
||||
|
||||
i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || currentSetItems == null));
|
||||
i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || currentSetItems == null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,40 +122,211 @@ namespace osu.Game.Screens.SelectV2
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private GroupDefinition? createGroupIfRequired(FilterCriteria criteria, BeatmapInfo beatmap, GroupDefinition? lastGroup)
|
||||
private List<GroupMapping> getGroups(List<CarouselItem> items, FilterCriteria criteria)
|
||||
{
|
||||
switch (criteria.Group)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
case GroupMode.All:
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
case GroupMode.NoGrouping:
|
||||
return new List<GroupMapping> { new GroupMapping(null, items) };
|
||||
|
||||
case GroupMode.Artist:
|
||||
char groupChar = lastGroup?.Data as char? ?? (char)0;
|
||||
char beatmapFirstChar = char.ToUpperInvariant(beatmap.Metadata.Artist[0]);
|
||||
return getGroupsBy(b => defineGroupAlphabetically(b.BeatmapSet!.Metadata.Artist), items);
|
||||
|
||||
if (beatmapFirstChar > groupChar)
|
||||
return new GroupDefinition(beatmapFirstChar, $"{beatmapFirstChar}");
|
||||
case GroupMode.Author:
|
||||
return getGroupsBy(b => defineGroupAlphabetically(b.BeatmapSet!.Metadata.Author.Username), items);
|
||||
|
||||
break;
|
||||
case GroupMode.Title:
|
||||
return getGroupsBy(b => defineGroupAlphabetically(b.BeatmapSet!.Metadata.Title), items);
|
||||
|
||||
case GroupMode.DateAdded:
|
||||
return getGroupsBy(b => defineGroupByDate(b.BeatmapSet!.DateAdded), items);
|
||||
|
||||
case GroupMode.LastPlayed:
|
||||
return getGroupsBy(b =>
|
||||
{
|
||||
DateTimeOffset? maxLastPlayed = aggregateMax(b, items, bb => bb.LastPlayed);
|
||||
|
||||
if (maxLastPlayed == null)
|
||||
return new GroupDefinition(int.MaxValue, "Never");
|
||||
|
||||
return defineGroupByDate(maxLastPlayed.Value);
|
||||
}, items);
|
||||
|
||||
case GroupMode.RankedStatus:
|
||||
return getGroupsBy(b => defineGroupByStatus(b.BeatmapSet!.Status), items);
|
||||
|
||||
case GroupMode.BPM:
|
||||
return getGroupsBy(b =>
|
||||
{
|
||||
double maxBPM = aggregateMax(b, items, bb => bb.BPM);
|
||||
return defineGroupByBPM(maxBPM);
|
||||
}, items);
|
||||
|
||||
case GroupMode.Difficulty:
|
||||
var starGroup = lastGroup?.Data as StarDifficulty? ?? new StarDifficulty(-1, 0);
|
||||
double beatmapStarRating = Math.Round(beatmap.StarRating, 2);
|
||||
return getGroupsBy(b => defineGroupByStars(b.StarRating), items);
|
||||
|
||||
if (beatmapStarRating >= starGroup.Stars + 1)
|
||||
case GroupMode.Length:
|
||||
return getGroupsBy(b =>
|
||||
{
|
||||
starGroup = new StarDifficulty((int)Math.Floor(beatmapStarRating), 0);
|
||||
double maxLength = aggregateMax(b, items, bb => bb.Length);
|
||||
return defineGroupByLength(maxLength);
|
||||
}, items);
|
||||
|
||||
if (starGroup.Stars == 0)
|
||||
return new GroupDefinition(starGroup, "Below 1 Star");
|
||||
case GroupMode.Collections:
|
||||
// todo: unsupported.
|
||||
goto case GroupMode.NoGrouping;
|
||||
|
||||
if (starGroup.Stars == 1)
|
||||
return new GroupDefinition(starGroup, "1 Star");
|
||||
case GroupMode.Favourites:
|
||||
// todo: unsupported.
|
||||
goto case GroupMode.NoGrouping;
|
||||
|
||||
return new GroupDefinition(starGroup, $"{starGroup.Stars} Stars");
|
||||
}
|
||||
case GroupMode.MyMaps:
|
||||
// todo: unsupported.
|
||||
goto case GroupMode.NoGrouping;
|
||||
|
||||
break;
|
||||
case GroupMode.RankAchieved:
|
||||
// todo: unsupported.
|
||||
goto case GroupMode.NoGrouping;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private List<GroupMapping> getGroupsBy(Func<BeatmapInfo, GroupDefinition> getGroup, List<CarouselItem> items)
|
||||
{
|
||||
return items.GroupBy(i => getGroup((BeatmapInfo)i.Model))
|
||||
.OrderBy(s => s.Key.Order)
|
||||
.Select(g => new GroupMapping(g.Key, g.ToList()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupAlphabetically(string name)
|
||||
{
|
||||
char firstChar = name.FirstOrDefault();
|
||||
|
||||
if (char.IsAsciiDigit(firstChar))
|
||||
return new GroupDefinition(int.MinValue, "0-9");
|
||||
|
||||
if (char.IsAsciiLetter(firstChar))
|
||||
return new GroupDefinition(char.ToUpperInvariant(firstChar) - 'A', char.ToUpperInvariant(firstChar).ToString());
|
||||
|
||||
return new GroupDefinition(int.MaxValue, "Other");
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByDate(DateTimeOffset date)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
var elapsed = now - date;
|
||||
|
||||
if (elapsed.TotalDays < 1)
|
||||
return new GroupDefinition(0, "Today");
|
||||
|
||||
if (elapsed.TotalDays < 2)
|
||||
return new GroupDefinition(1, "Yesterday");
|
||||
|
||||
if (elapsed.TotalDays < 7)
|
||||
return new GroupDefinition(2, "Last week");
|
||||
|
||||
if (elapsed.TotalDays < 30)
|
||||
return new GroupDefinition(3, "1 month ago");
|
||||
|
||||
for (int i = 60; i <= 150; i += 30)
|
||||
{
|
||||
if (elapsed.TotalDays < i)
|
||||
return new GroupDefinition(i, $"{i / 30} months ago");
|
||||
}
|
||||
|
||||
return null;
|
||||
return new GroupDefinition(int.MaxValue, "Over 5 months ago");
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByStatus(BeatmapOnlineStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case BeatmapOnlineStatus.Ranked:
|
||||
case BeatmapOnlineStatus.Approved:
|
||||
return new GroupDefinition(0, BeatmapOnlineStatus.Ranked.GetDescription());
|
||||
|
||||
case BeatmapOnlineStatus.Qualified:
|
||||
return new GroupDefinition(1, status.GetDescription());
|
||||
|
||||
case BeatmapOnlineStatus.WIP:
|
||||
return new GroupDefinition(2, status.GetDescription());
|
||||
|
||||
case BeatmapOnlineStatus.Pending:
|
||||
return new GroupDefinition(3, status.GetDescription());
|
||||
|
||||
case BeatmapOnlineStatus.Graveyard:
|
||||
return new GroupDefinition(4, status.GetDescription());
|
||||
|
||||
case BeatmapOnlineStatus.LocallyModified:
|
||||
return new GroupDefinition(5, status.GetDescription());
|
||||
|
||||
case BeatmapOnlineStatus.None:
|
||||
return new GroupDefinition(6, status.GetDescription());
|
||||
|
||||
case BeatmapOnlineStatus.Loved:
|
||||
return new GroupDefinition(7, status.GetDescription());
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(status), status, null);
|
||||
}
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByBPM(double bpm)
|
||||
{
|
||||
for (int i = 1; i < 6; i++)
|
||||
{
|
||||
if (bpm < i * 60)
|
||||
return new GroupDefinition(i, $"Under {i * 60} BPM");
|
||||
}
|
||||
|
||||
return new GroupDefinition(6, "Over 300 BPM");
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByStars(double stars)
|
||||
{
|
||||
int starInt = (int)Math.Round(stars, 2);
|
||||
var groupData = new StarDifficulty(starInt, 0);
|
||||
|
||||
if (starInt == 0)
|
||||
return new GroupDefinition(0, "Below 1 Star", groupData);
|
||||
|
||||
if (starInt == 1)
|
||||
return new GroupDefinition(1, "1 Star", groupData);
|
||||
|
||||
return new GroupDefinition(starInt, $"{starInt} Stars", groupData);
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByLength(double length)
|
||||
{
|
||||
for (int i = 1; i < 6; i++)
|
||||
{
|
||||
if (length <= i * 60_000)
|
||||
{
|
||||
if (i == 1)
|
||||
return new GroupDefinition(1, "1 minute or less");
|
||||
|
||||
return new GroupDefinition(i, $"{i} minutes or less");
|
||||
}
|
||||
}
|
||||
|
||||
if (length <= 10 * 60_000)
|
||||
return new GroupDefinition(10, "10 minutes or less");
|
||||
|
||||
return new GroupDefinition(11, "Over 10 minutes");
|
||||
}
|
||||
|
||||
private static T? aggregateMax<T>(BeatmapInfo b, IEnumerable<CarouselItem> items, Func<BeatmapInfo, T> func)
|
||||
{
|
||||
var matchedBeatmaps = items.Select(i => i.Model).Cast<BeatmapInfo>().Where(beatmap => beatmap.BeatmapSet!.Equals(b.BeatmapSet));
|
||||
return matchedBeatmaps.Max(func);
|
||||
}
|
||||
|
||||
private record GroupMapping(GroupDefinition? Group, List<CarouselItem> ItemsInGroup);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
var group = (GroupDefinition)Item.Model;
|
||||
var stars = (StarDifficulty)group.Data;
|
||||
var stars = (StarDifficulty)group.Data!;
|
||||
int starNumber = (int)stars.Stars;
|
||||
|
||||
ratingColour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber);
|
||||
|
||||
Reference in New Issue
Block a user