1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-05 17:43:00 +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"));
}
[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 class TestRuleset : Ruleset

View File

@ -4,12 +4,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Threading;
@ -18,7 +21,11 @@ using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
@ -237,10 +244,37 @@ namespace osu.Game.Beatmaps
var ruleset = rulesetInfo.CreateInstance();
Debug.Assert(ruleset != null);
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
var attributes = calculator.Calculate(key.OrderedMods, cancellationToken);
PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
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)
{
@ -276,7 +310,6 @@ namespace osu.Game.Beatmaps
{
public readonly BeatmapInfo BeatmapInfo;
public readonly RulesetInfo Ruleset;
public readonly Mod[] OrderedMods;
public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable<Mod>? mods)
@ -317,5 +350,42 @@ namespace osu.Game.Beatmaps
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty;
@ -25,30 +22,34 @@ namespace osu.Game.Beatmaps
/// 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.
/// </summary>
[CanBeNull]
public readonly DifficultyAttributes Attributes;
public readonly DifficultyAttributes? DifficultyAttributes;
/// <summary>
/// Creates a <see cref="StarDifficulty"/> structure based on <see cref="DifficultyAttributes"/> computed
/// by a <see cref="DifficultyCalculator"/>.
/// The performance attributes computed for a perfect score on the given beatmap.
/// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
/// </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;
MaxCombo = attributes.MaxCombo;
Attributes = attributes;
Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0;
MaxCombo = difficulty.MaxCombo;
DifficultyAttributes = difficulty;
PerformanceAttributes = performance;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
}
/// <summary>
/// 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>
public StarDifficulty(double starDifficulty, int maxCombo)
{
Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
MaxCombo = maxCombo;
Attributes = null;
}
public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);

View File

@ -12,23 +12,28 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary>
/// "Attribute"
/// </summary>
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute");
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute");
/// <summary>
/// "The attribute to be displayed."
/// </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>
/// "Template"
/// </summary>
public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template");
public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template");
/// <summary>
/// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."
/// </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).");
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>
public long MaximumTotalScore { get; private set; }
/// <summary>
/// The maximum achievable combo.
/// </summary>
public int MaximumCombo { get; private set; }
/// <summary>
/// The maximum sum of accuracy-affecting judgements at the current point in time.
/// </summary>
@ -423,6 +428,7 @@ namespace osu.Game.Rulesets.Scoring
MaximumResultCounts.AddRange(ScoreResultCounts);
MaximumTotalScore = TotalScore.Value;
MaximumCombo = HighestCombo.Value;
}
ScoreResultCounts.Clear();

View File

@ -53,10 +53,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
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)
if (attributes?.DifficultyAttributes == null || performanceCalculator == null)
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));
}, cancellationToken ?? default);

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
@ -26,7 +27,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public partial class PerformanceBreakdownChart : Container
{
private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
private Drawable spinner = null!;
private Drawable content = null!;
@ -42,7 +42,6 @@ namespace osu.Game.Screens.Ranking.Statistics
public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap)
{
this.score = score;
this.playableBeatmap = playableBeatmap;
}
[BackgroundDependencyLoader]
@ -142,12 +141,33 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Show();
new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache)
.CalculateAsync(score, cancellationTokenSource.Token)
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()!)));
computePerformance(cancellationTokenSource.Token)
.ContinueWith(t => Schedule(() =>
{
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();
content.FadeIn(200);
@ -236,6 +256,8 @@ namespace osu.Game.Screens.Ranking.Statistics
protected override void Dispose(bool isDisposing)
{
cancellationTokenSource.Cancel();
cancellationTokenSource.Dispose();
base.Dispose(isDisposing);
}
}

View File

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