1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 05:49:52 +08:00
Files
osu-lazer/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
T
Bartłomiej Dach 8cb81974eb Add initial support for filtering by user tags in song select
The way that this works is that it plugs into the online request to
retrieve the beatmap set that the client is already performing, and
stores user tag data to the local realm database.

This means that for now user tags will only populate for beatmaps that
the user has displayed on song select which is obviously subpar. I plan
to follow this change up by adding user tag state dumps to `online.db`
and using that data for initial tag population to make the majority case
(ranked beatmaps) work.

Note that several decisions were made here that are potential discussion
points:

- `RealmPopulatingOnlineLookupSource` is set up such that it can be the
  middle man / redirection point for similar flows that we need and we
  are currently missing, such as storing guest difficulty information,
  or storing the user's current best score on a beatmap (handy for rank
  achieved sorting / filtering / etc.)

- The user tags are stored in `BeatmapMetadata` which breaks the
  longstanding assumption that you can arbitrarily pull out a metadata
  instance from any of the beatmaps in a set and get essentially the
  same object back.

  I've attempted to constrain this some by not adding user tags to
  the `IBeatmapMetadataInfo` interface through which `BeatmapSetInfo`
  exposes metadata further, but I warn in advance that this is
  a temporary state of affairs and I will make it worse in the future
  when `BeatmapMetadata.Author` becomes `Authors` plural in order to
  support guest mapper display (and direct guest difficulty submission).

- The syntax for searching via user tags is chosen to mostly match web -
  it's `tag=`, with support for all of the string matching modes song
  select already has (bare word for substring, `""` quotes for phrase
  isolated by whitespace, `""!` for exact full match).
2025-07-15 11:09:40 +02:00

132 lines
6.1 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.Linq;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
using osu.Game.Utils;
namespace osu.Game.Screens.Select.Carousel
{
public class CarouselBeatmap : CarouselItem
{
public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT;
public readonly BeatmapInfo BeatmapInfo;
public CarouselBeatmap(BeatmapInfo beatmapInfo)
{
BeatmapInfo = beatmapInfo;
State.Value = CarouselItemState.Collapsed;
}
public override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this);
public override void Filter(FilterCriteria criteria)
{
base.Filter(criteria);
Filtered.Value = !checkMatch(criteria);
}
private bool checkMatch(FilterCriteria criteria)
{
bool match =
criteria.Ruleset == null ||
BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName ||
(BeatmapInfo.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps);
if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
{
// only check ruleset equality or convertability for selected beatmap
return match;
}
if (!match) return false;
if (criteria.SearchTerms.Length > 0)
{
match = BeatmapInfo.Match(criteria.SearchTerms);
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
// this should be done after text matching so we can prioritise matching numbers in metadata.
if (!match && criteria.SearchNumber.HasValue)
{
match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) ||
(BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value);
}
}
if (!match) return false;
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating.FloorToDecimalDigits(2));
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate);
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate);
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.Difficulty.CircleSize);
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty);
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length);
match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(BeatmapInfo.LastPlayed ?? DateTimeOffset.MinValue);
match &= !criteria.DateRanked.HasFilter || (BeatmapInfo.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(BeatmapInfo.BeatmapSet.DateRanked.Value));
match &= !criteria.DateSubmitted.HasFilter || (BeatmapInfo.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(BeatmapInfo.BeatmapSet.DateSubmitted.Value));
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM);
match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);
match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status);
if (!match) return false;
match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username);
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) ||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName);
match &= !criteria.Source.HasFilter || criteria.Source.Matches(BeatmapInfo.Metadata.Source);
if (criteria.UserTag.HasFilter)
{
bool anyTagMatched = false;
foreach (string tag in BeatmapInfo.Metadata.UserTags)
anyTagMatched |= criteria.UserTag.Matches(tag);
match &= anyTagMatched;
}
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
if (!match) return false;
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria);
if (match && criteria.HasOnlineID == true)
match &= BeatmapInfo.OnlineID >= 0;
if (match && criteria.BeatmapSetId != null)
match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID;
return match;
}
public override int CompareTo(FilterCriteria criteria, CarouselItem other)
{
if (!(other is CarouselBeatmap otherBeatmap))
return base.CompareTo(criteria, other);
switch (criteria.Sort)
{
default:
case SortMode.Difficulty:
int ruleset = BeatmapInfo.Ruleset.CompareTo(otherBeatmap.BeatmapInfo.Ruleset);
if (ruleset != 0) return ruleset;
return BeatmapInfo.StarRating.CompareTo(otherBeatmap.BeatmapInfo.StarRating);
}
}
public override string ToString() => BeatmapInfo.ToString();
}
}