1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-05 22:52:58 +08:00

Merge pull request #30322 from smoogipoo/bat-max-performance

Implement "max pp" beatmap difficulty attribute text
This commit is contained in:
Dean Herbert 2024-11-14 14:18:36 +09:00 committed by GitHub
commit 5cc1cbe880
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 157 additions and 150 deletions

View File

@ -159,6 +159,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00")); AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00"));
} }
[Test]
public void TestMaxPp()
{
AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo);
AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP);
AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0"));
// Adding mod
TestMod mod = null!;
AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } });
AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1"));
// Changing mod setting
AddStep("change mod pp to 2", () => mod.Performance.Value = 2);
AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2"));
}
private string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString(); private string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString();
private class TestRuleset : Ruleset private class TestRuleset : Ruleset

View File

@ -4,12 +4,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -18,7 +21,11 @@ using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -237,10 +244,37 @@ namespace osu.Game.Beatmaps
var ruleset = rulesetInfo.CreateInstance(); var ruleset = rulesetInfo.CreateInstance();
Debug.Assert(ruleset != null); Debug.Assert(ruleset != null);
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
var attributes = calculator.Calculate(key.OrderedMods, cancellationToken); IBeatmap playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, key.OrderedMods, cancellationToken);
return new StarDifficulty(attributes); var difficulty = ruleset.CreateDifficultyCalculator(workingBeatmap).Calculate(key.OrderedMods, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
var performanceCalculator = ruleset.CreatePerformanceCalculator();
if (performanceCalculator == null)
return new StarDifficulty(difficulty, new PerformanceAttributes());
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = key.OrderedMods;
scoreProcessor.ApplyBeatmap(playableBeatmap);
cancellationToken.ThrowIfCancellationRequested();
ScoreInfo perfectScore = new ScoreInfo(key.BeatmapInfo, ruleset.RulesetInfo)
{
Passed = true,
Accuracy = 1,
Mods = key.OrderedMods,
MaxCombo = scoreProcessor.MaximumCombo,
Combo = scoreProcessor.MaximumCombo,
TotalScore = scoreProcessor.MaximumTotalScore,
Statistics = scoreProcessor.MaximumStatistics,
MaximumStatistics = scoreProcessor.MaximumStatistics
};
var performance = performanceCalculator.Calculate(perfectScore, difficulty);
cancellationToken.ThrowIfCancellationRequested();
return new StarDifficulty(difficulty, performance);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -276,7 +310,6 @@ namespace osu.Game.Beatmaps
{ {
public readonly BeatmapInfo BeatmapInfo; public readonly BeatmapInfo BeatmapInfo;
public readonly RulesetInfo Ruleset; public readonly RulesetInfo Ruleset;
public readonly Mod[] OrderedMods; public readonly Mod[] OrderedMods;
public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable<Mod>? mods) public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable<Mod>? mods)
@ -317,5 +350,42 @@ namespace osu.Game.Beatmaps
CancellationToken = cancellationToken; CancellationToken = cancellationToken;
} }
} }
/// <summary>
/// A working beatmap that caches its playable representation.
/// This is intended as single-use for when it is guaranteed that the playable beatmap can be reused.
/// </summary>
private class PlayableCachedWorkingBeatmap : IWorkingBeatmap
{
private readonly IWorkingBeatmap working;
private IBeatmap? playable;
public PlayableCachedWorkingBeatmap(IWorkingBeatmap working)
{
this.working = working;
}
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
=> playable ??= working.GetPlayableBeatmap(ruleset, mods);
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods, CancellationToken cancellationToken)
=> playable ??= working.GetPlayableBeatmap(ruleset, mods, cancellationToken);
IBeatmapInfo IWorkingBeatmap.BeatmapInfo => working.BeatmapInfo;
bool IWorkingBeatmap.BeatmapLoaded => working.BeatmapLoaded;
bool IWorkingBeatmap.TrackLoaded => working.TrackLoaded;
IBeatmap IWorkingBeatmap.Beatmap => working.Beatmap;
Texture IWorkingBeatmap.GetBackground() => working.GetBackground();
Texture IWorkingBeatmap.GetPanelBackground() => working.GetPanelBackground();
Waveform IWorkingBeatmap.Waveform => working.Waveform;
Storyboard IWorkingBeatmap.Storyboard => working.Storyboard;
ISkin IWorkingBeatmap.Skin => working.Skin;
Track IWorkingBeatmap.Track => working.Track;
Track IWorkingBeatmap.LoadTrack() => working.LoadTrack();
Stream IWorkingBeatmap.GetStream(string storagePath) => working.GetStream(storagePath);
void IWorkingBeatmap.BeginAsyncLoad() => working.BeginAsyncLoad();
void IWorkingBeatmap.CancelAsyncLoad() => working.CancelAsyncLoad();
void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint);
}
} }
} }

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -25,30 +22,34 @@ namespace osu.Game.Beatmaps
/// The difficulty attributes computed for the given beatmap. /// The difficulty attributes computed for the given beatmap.
/// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
/// </summary> /// </summary>
[CanBeNull] public readonly DifficultyAttributes? DifficultyAttributes;
public readonly DifficultyAttributes Attributes;
/// <summary> /// <summary>
/// Creates a <see cref="StarDifficulty"/> structure based on <see cref="DifficultyAttributes"/> computed /// The performance attributes computed for a perfect score on the given beatmap.
/// by a <see cref="DifficultyCalculator"/>. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
/// </summary> /// </summary>
public StarDifficulty([NotNull] DifficultyAttributes attributes) public readonly PerformanceAttributes? PerformanceAttributes;
/// <summary>
/// Creates a <see cref="StarDifficulty"/> structure.
/// </summary>
public StarDifficulty(DifficultyAttributes difficulty, PerformanceAttributes performance)
{ {
Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0; Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0;
MaxCombo = attributes.MaxCombo; MaxCombo = difficulty.MaxCombo;
Attributes = attributes; DifficultyAttributes = difficulty;
PerformanceAttributes = performance;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
} }
/// <summary> /// <summary>
/// Creates a <see cref="StarDifficulty"/> structure with a pre-populated star difficulty and max combo /// Creates a <see cref="StarDifficulty"/> structure with a pre-populated star difficulty and max combo
/// in scenarios where computing <see cref="DifficultyAttributes"/> is not feasible (i.e. when working with online sources). /// in scenarios where computing <see cref="Rulesets.Difficulty.DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
/// </summary> /// </summary>
public StarDifficulty(double starDifficulty, int maxCombo) public StarDifficulty(double starDifficulty, int maxCombo)
{ {
Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0; Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
MaxCombo = maxCombo; MaxCombo = maxCombo;
Attributes = null;
} }
public DifficultyRating DifficultyRating => GetDifficultyRating(Stars); public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);

View File

@ -12,23 +12,28 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary> /// <summary>
/// "Attribute" /// "Attribute"
/// </summary> /// </summary>
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute"); public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute");
/// <summary> /// <summary>
/// "The attribute to be displayed." /// "The attribute to be displayed."
/// </summary> /// </summary>
public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), "The attribute to be displayed."); public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), @"The attribute to be displayed.");
/// <summary> /// <summary>
/// "Template" /// "Template"
/// </summary> /// </summary>
public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template"); public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template");
/// <summary> /// <summary>
/// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)." /// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."
/// </summary> /// </summary>
public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."); public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values).");
private static string getKey(string key) => $"{prefix}:{key}"; /// <summary>
/// "Max PP"
/// </summary>
public static LocalisableString MaxPP => new TranslatableString(getKey(@"max_pp"), @"Max PP");
private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -1,121 +0,0 @@
// 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.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Difficulty
{
public class PerformanceBreakdownCalculator
{
private readonly IBeatmap playableBeatmap;
private readonly BeatmapDifficultyCache difficultyCache;
public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache)
{
this.playableBeatmap = playableBeatmap;
this.difficultyCache = difficultyCache;
}
[ItemCanBeNull]
public async Task<PerformanceBreakdown> CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default)
{
var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
if (attributes?.Attributes == null || performanceCalculator == null)
return null;
cancellationToken.ThrowIfCancellationRequested();
PerformanceAttributes[] performanceArray = await Task.WhenAll(
// compute actual performance
performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken),
// compute performance for perfect play
getPerfectPerformance(score, cancellationToken)
).ConfigureAwait(false);
return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes());
}
[ItemCanBeNull]
private Task<PerformanceAttributes> getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default)
{
return Task.Run(async () =>
{
Ruleset ruleset = score.Ruleset.CreateInstance();
ScoreInfo perfectPlay = score.DeepClone();
perfectPlay.Accuracy = 1;
perfectPlay.Passed = true;
// calculate max combo
// todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores
perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap);
// create statistics assuming all hit objects have perfect hit result
var statistics = playableBeatmap.HitObjects
.SelectMany(getPerfectHitResults)
.GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
.ToDictionary(pair => pair.hitResult, pair => pair.count);
perfectPlay.Statistics = statistics;
perfectPlay.MaximumStatistics = statistics;
// calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = perfectPlay.Mods;
scoreProcessor.ApplyBeatmap(playableBeatmap);
perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore;
// compute rank achieved
// default to SS, then adjust the rank with mods
perfectPlay.Rank = ScoreRank.X;
foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType<IApplicableToScoreProcessor>())
{
perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1);
}
// calculate performance for this perfect score
var difficulty = await difficultyCache.GetDifficultyAsync(
playableBeatmap.BeatmapInfo,
score.Ruleset,
score.Mods,
cancellationToken
).ConfigureAwait(false);
var performanceCalculator = ruleset.CreatePerformanceCalculator();
if (performanceCalculator == null || difficulty == null)
return null;
return await performanceCalculator.CalculateAsync(perfectPlay, difficulty.Value.Attributes.AsNonNull(), cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}
private int calculateMaxCombo(IBeatmap beatmap)
{
return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo());
}
private IEnumerable<HitResult> getPerfectHitResults(HitObject hitObject)
{
foreach (HitObject nested in hitObject.NestedHitObjects)
yield return nested.Judgement.MaxResult;
yield return hitObject.Judgement.MaxResult;
}
}
}

View File

@ -119,6 +119,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
public long MaximumTotalScore { get; private set; } public long MaximumTotalScore { get; private set; }
/// <summary>
/// The maximum achievable combo.
/// </summary>
public int MaximumCombo { get; private set; }
/// <summary> /// <summary>
/// The maximum sum of accuracy-affecting judgements at the current point in time. /// The maximum sum of accuracy-affecting judgements at the current point in time.
/// </summary> /// </summary>
@ -423,6 +428,7 @@ namespace osu.Game.Rulesets.Scoring
MaximumResultCounts.AddRange(ScoreResultCounts); MaximumResultCounts.AddRange(ScoreResultCounts);
MaximumTotalScore = TotalScore.Value; MaximumTotalScore = TotalScore.Value;
MaximumCombo = HighestCombo.Value;
} }
ScoreResultCounts.Clear(); ScoreResultCounts.Clear();

View File

@ -53,10 +53,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
if (attributes?.Attributes == null || performanceCalculator == null) if (attributes?.DifficultyAttributes == null || performanceCalculator == null)
return; return;
var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false); var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false);
Schedule(() => setPerformanceValue(score, result.Total)); Schedule(() => setPerformanceValue(score, result.Total));
}, cancellationToken ?? default); }, cancellationToken ?? default);

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -26,7 +27,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public partial class PerformanceBreakdownChart : Container public partial class PerformanceBreakdownChart : Container
{ {
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
private Drawable spinner = null!; private Drawable spinner = null!;
private Drawable content = null!; private Drawable content = null!;
@ -42,7 +42,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap)
{ {
this.score = score; this.score = score;
this.playableBeatmap = playableBeatmap;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -142,12 +141,33 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Show(); spinner.Show();
new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache) computePerformance(cancellationTokenSource.Token)
.CalculateAsync(score, cancellationTokenSource.Token) .ContinueWith(t => Schedule(() =>
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()!))); {
if (t.GetResultSafely() is PerformanceBreakdown breakdown)
setPerformance(breakdown);
}), TaskContinuationOptions.OnlyOnRanToCompletion);
} }
private void setPerformanceValue(PerformanceBreakdown breakdown) private async Task<PerformanceBreakdown?> computePerformance(CancellationToken token)
{
var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
if (performanceCalculator == null)
return null;
var starsTask = difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false);
if (await starsTask is not StarDifficulty stars)
return null;
if (stars.DifficultyAttributes == null || stars.PerformanceAttributes == null)
return null;
return new PerformanceBreakdown(
await performanceCalculator.CalculateAsync(score, stars.DifficultyAttributes, token).ConfigureAwait(false),
stars.PerformanceAttributes);
}
private void setPerformance(PerformanceBreakdown breakdown)
{ {
spinner.Hide(); spinner.Hide();
content.FadeIn(200); content.FadeIn(200);
@ -236,6 +256,8 @@ namespace osu.Game.Screens.Ranking.Statistics
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
cancellationTokenSource.Dispose();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
} }

View File

@ -177,6 +177,9 @@ namespace osu.Game.Skinning.Components
case BeatmapAttribute.BPM: case BeatmapAttribute.BPM:
return BeatmapsetsStrings.ShowStatsBpm; return BeatmapsetsStrings.ShowStatsBpm;
case BeatmapAttribute.MaxPP:
return BeatmapAttributeTextStrings.MaxPP;
default: default:
return string.Empty; return string.Empty;
} }
@ -225,6 +228,9 @@ namespace osu.Game.Skinning.Components
case BeatmapAttribute.StarRating: case BeatmapAttribute.StarRating:
return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2");
case BeatmapAttribute.MaxPP:
return Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, MidpointRounding.AwayFromZero).ToLocalisableString();
default: default:
return string.Empty; return string.Empty;
} }
@ -279,5 +285,6 @@ namespace osu.Game.Skinning.Components
RankedStatus, RankedStatus,
BPM, BPM,
Source, Source,
MaxPP
} }
} }