1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 21:40:56 +08:00
Files
osu-lazer/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
T
Bartłomiej Dach d9e182230d Add bool flag for checking tag vote threshold & utilise as required
Closes https://github.com/ppy/osu/issues/36453.

My omission was in assuming that web was going to start filtering out
the tags below the threshold from API responses, which is not the case.

Whether or not the consumers want or not to display tags below threshold
is subjective. I figured that the matchmaking card tooltip might want to
display below threshold but I dunno.
2026-03-03 13:03:33 +01:00

231 lines
7.7 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 Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIBeatmap : IBeatmapInfo, IBeatmapOnlineInfo
{
[JsonProperty(@"id")]
public int OnlineID { get; set; }
[JsonProperty(@"beatmapset_id")]
public int OnlineBeatmapSetID { get; set; }
[JsonProperty(@"status")]
public BeatmapOnlineStatus Status { get; set; }
[JsonProperty("checksum")]
public string Checksum { get; set; } = string.Empty;
[JsonProperty(@"user_id")]
public int AuthorID { get; set; }
[JsonProperty(@"beatmapset")]
public APIBeatmapSet? BeatmapSet { get; set; }
[JsonProperty(@"playcount")]
public int PlayCount { get; set; }
[JsonProperty(@"current_user_playcount")]
public int UserPlayCount { get; set; }
[JsonProperty(@"passcount")]
public int PassCount { get; set; }
[JsonProperty(@"mode_int")]
public int RulesetID { get; set; }
[JsonProperty(@"difficulty_rating")]
public double StarRating { get; set; }
public int EndTimeObjectCount => SliderCount + SpinnerCount;
public int TotalObjectCount => CircleCount + SliderCount + SpinnerCount;
[JsonProperty(@"drain")]
public float DrainRate { get; set; }
[JsonProperty(@"cs")]
public float CircleSize { get; set; }
[JsonProperty(@"ar")]
public float ApproachRate { get; set; }
[JsonProperty(@"accuracy")]
public float OverallDifficulty { get; set; }
[JsonIgnore]
public double Length { get; set; }
[JsonProperty(@"total_length")]
private double lengthInSeconds
{
get => TimeSpan.FromMilliseconds(Length).TotalSeconds;
set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds;
}
[JsonIgnore]
public double HitLength { get; set; }
[JsonProperty(@"hit_length")]
private double hitLengthInSeconds
{
get => TimeSpan.FromMilliseconds(HitLength).TotalSeconds;
set => HitLength = TimeSpan.FromSeconds(value).TotalMilliseconds;
}
[JsonProperty(@"convert")]
public bool Convert { get; set; }
[JsonProperty(@"count_circles")]
public int CircleCount { get; set; }
[JsonProperty(@"count_sliders")]
public int SliderCount { get; set; }
[JsonProperty(@"count_spinners")]
public int SpinnerCount { get; set; }
[JsonProperty(@"version")]
public string DifficultyName { get; set; } = string.Empty;
[JsonProperty(@"failtimes")]
public APIFailTimes? FailTimes { get; set; }
[JsonProperty(@"top_tag_ids")]
public APIBeatmapTag[]? TopTags { get; set; }
[JsonProperty(@"current_user_tag_ids")]
public long[]? OwnTagIds { get; set; }
[JsonProperty(@"max_combo")]
public int? MaxCombo { get; set; }
[JsonProperty(@"last_updated")]
public DateTimeOffset LastUpdated { get; set; }
public double BPM { get; set; }
[JsonProperty(@"owners")]
public BeatmapOwner[] BeatmapOwners { get; set; } = Array.Empty<BeatmapOwner>();
/// <summary>
/// Minimum count of votes required to display a tag on the beatmap's page.
/// Should match value specified web-side as https://github.com/ppy/osu-web/blob/cae2fdf03cfb8c30c8e332cfb142e03188ceffef/config/osu.php#L59.
/// </summary>
public const int MINIMUM_USER_TAG_VOTES_FOR_DISPLAY = 5;
/// <summary>
/// Retrieves top user tags for the beatmap, ordered in a way matching osu!web.
/// Requires <see cref="BeatmapSet"/> to be populated.
/// </summary>
/// <param name="confirmedOnly">
/// If <see langword="true"/>, only tags above <see cref="MINIMUM_USER_TAG_VOTES_FOR_DISPLAY"/> will be shown.
/// If <see langword="false"/>, all tags regardless of vote count will be shown.
/// </param>
public (APITag Tag, int VoteCount)[] GetTopUserTags(bool confirmedOnly)
{
if (TopTags == null || TopTags.Length == 0 || BeatmapSet?.RelatedTags == null)
return [];
var tagsById = BeatmapSet.RelatedTags.ToDictionary(t => t.Id);
return TopTags
.Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId)))
.Where(t => t.relatedTag != null && (!confirmedOnly || t.topTag.VoteCount >= MINIMUM_USER_TAG_VOTES_FOR_DISPLAY))
// see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria
.OrderByDescending(t => t.topTag.VoteCount)
.ThenBy(t => t.relatedTag!.Name)
.Select(t => (t.relatedTag!, t.topTag.VoteCount))
.ToArray();
}
#region Implementation of IBeatmapInfo
public IBeatmapMetadataInfo Metadata => (BeatmapSet as IBeatmapSetInfo)?.Metadata ?? new BeatmapMetadata();
public IBeatmapDifficultyInfo Difficulty => new BeatmapDifficulty
{
DrainRate = DrainRate,
CircleSize = CircleSize,
ApproachRate = ApproachRate,
OverallDifficulty = OverallDifficulty
};
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
public string MD5Hash => Checksum;
public IRulesetInfo Ruleset => new APIRuleset { OnlineID = RulesetID };
[JsonIgnore]
public string Hash => throw new NotImplementedException();
#endregion
public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b);
public class APIRuleset : IRulesetInfo
{
public int OnlineID { get; set; } = -1;
public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})";
[JsonIgnore]
public string ShortName
{
get
{
// TODO: this should really not exist.
switch (OnlineID)
{
case 0: return "osu";
case 1: return "taiko";
case 2: return "fruits";
case 3: return "mania";
default: throw new ArgumentOutOfRangeException();
}
}
}
public string InstantiationInfo => string.Empty;
public Ruleset CreateInstance() => throw new NotImplementedException();
public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r);
public int CompareTo(IRulesetInfo? other)
{
if (!(other is APIRuleset ruleset))
throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other));
return OnlineID.CompareTo(ruleset.OnlineID);
}
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => OnlineID;
}
public class BeatmapOwner
{
[JsonProperty(@"id")]
public int Id { get; set; }
[JsonProperty(@"username")]
public string Username { get; set; } = string.Empty;
}
}
}