// 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.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { public override float TotalHeight { get { switch (State.Value) { case CarouselItemState.Selected: return DrawableCarouselBeatmapSet.HEIGHT + Items.Count(c => c.Visible) * DrawableCarouselBeatmap.HEIGHT; default: return DrawableCarouselBeatmapSet.HEIGHT; } } } public IEnumerable Beatmaps => Items.OfType(); public BeatmapSetInfo BeatmapSet; public Func, BeatmapInfo?>? GetRecommendedBeatmap; public CarouselBeatmapSet(BeatmapSetInfo beatmapSet) { BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet)); beatmapSet.Beatmaps .Where(b => !b.Hidden) .OrderBy(b => b.Ruleset) .ThenBy(b => b.StarRating) .Select(b => new CarouselBeatmap(b)) .ForEach(AddItem); } protected override CarouselItem? GetNextToSelect() { if (LastSelected == null || LastSelected.Filtered.Value) { if (GetRecommendedBeatmap?.Invoke(Items.OfType().Where(b => !b.Filtered.Value).Select(b => b.BeatmapInfo)) is BeatmapInfo recommended) return Items.OfType().First(b => b.BeatmapInfo.Equals(recommended)); } return base.GetNextToSelect(); } public override int CompareTo(FilterCriteria criteria, CarouselItem other) { if (!(other is CarouselBeatmapSet otherSet)) return base.CompareTo(criteria, other); int comparison = 0; switch (criteria.Sort) { default: case SortMode.Artist: comparison = string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase); break; case SortMode.Title: comparison = string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase); break; case SortMode.Author: comparison = string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase); break; case SortMode.Source: comparison = string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase); break; case SortMode.DateAdded: comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); break; case SortMode.DateRanked: // Beatmaps which have no ranked date should already be filtered away in this mode. if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null) break; comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value); break; case SortMode.LastPlayed: comparison = -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds()); break; case SortMode.BPM: comparison = compareUsingAggregateMax(otherSet, b => b.BPM); break; case SortMode.Length: comparison = compareUsingAggregateMax(otherSet, b => b.Length); break; case SortMode.Difficulty: comparison = compareUsingAggregateMax(otherSet, b => b.StarRating); break; case SortMode.DateSubmitted: // Beatmaps which have no submitted date should already be filtered away in this mode. if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null) break; comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value); break; } if (comparison != 0) return comparison; // If the initial sort could not differentiate, attempt to use DateAdded to order sets in a stable fashion. // The directionality of this matches the current SortMode.DateAdded, but we may want to reconsider if that becomes a user decision (ie. asc / desc). comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); if (comparison != 0) return comparison; // If DateAdded fails to break the tie, fallback to our internal GUID for stability. // This basically means it's a stable random sort. return otherSet.BeatmapSet.ID.CompareTo(BeatmapSet.ID); } /// /// All beatmaps which are not filtered and valid for display. /// protected IEnumerable ValidBeatmaps => Beatmaps.Where(b => !b.Filtered.Value || b.State.Value == CarouselItemState.Selected).Select(b => b.BeatmapInfo); private int compareUsingAggregateMax(CarouselBeatmapSet other, Func func) { bool ourBeatmaps = ValidBeatmaps.Any(); bool otherBeatmaps = other.ValidBeatmaps.Any(); if (!ourBeatmaps && !otherBeatmaps) return 0; if (!ourBeatmaps) return -1; if (!otherBeatmaps) return 1; return ValidBeatmaps.Max(func).CompareTo(other.ValidBeatmaps.Max(func)); } public override void Filter(FilterCriteria criteria) { base.Filter(criteria); bool filtered = Items.All(i => i.Filtered.Value); filtered |= criteria.Sort == SortMode.DateRanked && BeatmapSet.DateRanked == null; filtered |= criteria.Sort == SortMode.DateSubmitted && BeatmapSet.DateSubmitted == null; Filtered.Value = filtered; } public override string ToString() => BeatmapSet.ToString(); } }