mirror of
https://github.com/ppy/osu.git
synced 2026-05-21 19:40:15 +08:00
9314ea94b5
* implement stuff * fix basic issues * rework calculations * sanity check * don't use score based misscount if no scorev1 present * Update OsuPerformanceCalculator.cs * update misscount diff attribute names * add raw score misscount attribute * introduce more reasonable high bound for misscount * code quality changes * Fix osu!catch SR buzz slider detection (#32412) * Use `normalized_hitobject_radius` during osu!catch buzz slider detection Currently the algorithm considers some buzz sliders as standstills when in reality they require movement. This happens because `HalfCatcherWidth` isn't normalized while `exactDistanceMoved` is, leading to an inaccurate comparison. `normalized_hitobject_radius` is the normalized value of `HalfCatcherWidth` and replacing one with the other fixes the problem. * Rename `normalized_hitobject_radius` to `normalized_half_catcher_width` The current name is confusing because hit objects have no radius in the context of osu!catch difficulty calculation. The new name conveys the actual purpose of the value. * Only set `normalized_half_catcher_width` in `CatchDifficultyHitObject` Prevents potential bugs if the value were to be changed in one of the classes but not in both. * Use `CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH` directly Requested during code review. --------- Co-authored-by: James Wilson <tsunyoku@gmail.com> * Move osu!catch movement diffcalc to an evaluator (#32655) * Move osu!catch movement state into `CatchDifficultyHitObject` In order to port `Movement` to an evaluator, the state has to be either moved elsewhere or calculated inside the evaluator. The latter requires backtracking for every hit object, which in the worst case is continued until the beginning of the map is reached. Limiting backtracking can lead to difficulty value changes. Thus, the first option was chosen for its simplicity. * Move osu!catch movement difficulty calculation to an evaluator Makes the code more in line with the other game modes. * Add documentation for `CatchDifficultyHitObject` fields --------- Co-authored-by: James Wilson <tsunyoku@gmail.com> * Move all score-independent bonuses into star rating (#31351) * basis refactor to allow for more complex SR calculations * move all possible bonuses into star rating * decrease star rating scaling to account for overall gains * add extra FL guard for safety * move star rating multiplier into a constant * Reorganise some things * Add HD and SO to difficulty adjustment mods * Move non-legacy mod multipliers back to PP * Some merge fixes * Fix application of flashlight rating multiplier * Fix Hidden bonuses being applied when Blinds mod is in use * Move part of speed OD scaling into difficulty * Move length bonus back to PP * Remove blinds special case * Revert star rating multiplier decrease * More balancing --------- Co-authored-by: StanR <hi@stanr.info> * Add diffcalc considerations for Magnetised mod (#33004) * Add diffcalc considerations for Magnetised mod * Make speed reduction scale with power too * cleaning up * Update OsuPerformanceCalculator.cs * Update OsuPerformanceCalculator.cs * add new check to avoid overestimation * fix code style * fix nvicka * add database attributes * Refactor * Rename `Working` to `WorkingBeatmap` * Remove redundant condition * Remove useless variable * Remove `get` wording * Rename `calculateScoreAtCombo` * Remove redundant operator * Add comments to explain how score-based miss count derivations work * Remove redundant `decimal` calculations * use static method to improve performance * move stuff around for readability * move logic into helper class * fix the bug * Delete OsuLegacyScoreProcessor.cs * Delete ILegacyScoreProcessor.cs * revert static method for multiplier * use only basic combo score attribute * Clean-up * Remove unused param * Update osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs Co-authored-by: StanR <castl@inbox.ru> * rename variables * Add `LegacyScoreUtils` * Add fail safe * Move `countMiss` * Better explain `CalculateRelevantScoreComboPerObject` * Add `OsuLegacyScoreMissCalculator` * Move `CalculateScoreAtCombo` and `CalculateRelevantScoreComboPerObject` * Remove unused variables * Move `GetLegacyScoreMultiplier` * Add `estimated` wording --------- Co-authored-by: wulpine <wulpine@proton.me> Co-authored-by: James Wilson <tsunyoku@gmail.com> Co-authored-by: StanR <hi@stanr.info> Co-authored-by: StanR <castl@inbox.ru>
433 lines
18 KiB
C#
433 lines
18 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.
|
|
|
|
#nullable disable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using JetBrains.Annotations;
|
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
using osu.Framework.Lists;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
using osu.Game.Beatmaps.Timing;
|
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
using osu.Game.Rulesets.Difficulty.Skills;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Utils;
|
|
|
|
namespace osu.Game.Rulesets.Difficulty
|
|
{
|
|
public abstract class DifficultyCalculator
|
|
{
|
|
/// <summary>
|
|
/// The beatmap for which difficulty will be calculated.
|
|
/// </summary>
|
|
protected IBeatmap Beatmap { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The working beatmap for which difficulty will be calculated.
|
|
/// </summary>
|
|
protected readonly IWorkingBeatmap WorkingBeatmap;
|
|
|
|
private Mod[] playableMods;
|
|
private double clockRate;
|
|
|
|
private readonly IRulesetInfo ruleset;
|
|
|
|
/// <summary>
|
|
/// A yymmdd version which is used to discern when reprocessing is required.
|
|
/// </summary>
|
|
public virtual int Version => 0;
|
|
|
|
protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
|
{
|
|
this.ruleset = ruleset;
|
|
WorkingBeatmap = beatmap;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap with no mods applied.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
|
public DifficultyAttributes Calculate(CancellationToken cancellationToken = default)
|
|
=> Calculate(Array.Empty<Mod>(), cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap using a specific mod combination.
|
|
/// </summary>
|
|
/// <param name="mods">The mods that should be applied to the beatmap.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
|
public DifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
|
|
{
|
|
using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
|
|
if (!cancellationToken.CanBeCanceled)
|
|
cancellationToken = timedCancellationSource.Token;
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
preProcess(mods, cancellationToken);
|
|
|
|
var skills = CreateSkills(Beatmap, playableMods, clockRate);
|
|
|
|
if (!Beatmap.HitObjects.Any())
|
|
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
|
|
|
|
foreach (var hitObject in getDifficultyHitObjects())
|
|
{
|
|
foreach (var skill in skills)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
skill.Process(hitObject);
|
|
}
|
|
}
|
|
|
|
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap with no mods applied and returns a set of <see cref="TimedDifficultyAttributes"/> representing the difficulty at every relevant time value in the beatmap.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
|
|
public List<TimedDifficultyAttributes> CalculateTimed(CancellationToken cancellationToken = default)
|
|
=> CalculateTimed(Array.Empty<Mod>(), cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap using a specific mod combination and returns a set of <see cref="TimedDifficultyAttributes"/> representing the difficulty at every relevant time value in the beatmap.
|
|
/// </summary>
|
|
/// <param name="mods">The mods that should be applied to the beatmap.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
|
|
public List<TimedDifficultyAttributes> CalculateTimed([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
|
|
{
|
|
using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
|
|
if (!cancellationToken.CanBeCanceled)
|
|
cancellationToken = timedCancellationSource.Token;
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
preProcess(mods, cancellationToken);
|
|
|
|
var attribs = new List<TimedDifficultyAttributes>();
|
|
|
|
if (!Beatmap.HitObjects.Any())
|
|
return attribs;
|
|
|
|
var skills = CreateSkills(Beatmap, playableMods, clockRate);
|
|
var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
|
|
var difficultyObjects = getDifficultyHitObjects().ToArray();
|
|
|
|
int currentIndex = 0;
|
|
|
|
foreach (var obj in Beatmap.HitObjects)
|
|
{
|
|
progressiveBeatmap.HitObjects.Add(obj);
|
|
|
|
while (currentIndex < difficultyObjects.Length && difficultyObjects[currentIndex].BaseObject.GetEndTime() <= obj.GetEndTime())
|
|
{
|
|
foreach (var skill in skills)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
skill.Process(difficultyObjects[currentIndex]);
|
|
}
|
|
|
|
currentIndex++;
|
|
}
|
|
|
|
attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
|
|
}
|
|
|
|
return attribs;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can only be used to compute difficulties for legacy mod combinations.
|
|
/// </remarks>
|
|
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
|
|
public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
|
|
{
|
|
var rulesetInstance = ruleset.CreateInstance();
|
|
|
|
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
|
{
|
|
Mod classicMod = rulesetInstance.CreateMod<ModClassic>();
|
|
|
|
var finalCombination = ModUtils.FlattenMod(combination);
|
|
if (classicMod != null)
|
|
finalCombination = finalCombination.Append(classicMod);
|
|
|
|
yield return Calculate(finalCombination.ToArray(), cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the <see cref="DifficultyHitObject"/>s to calculate against.
|
|
/// </summary>
|
|
private IEnumerable<DifficultyHitObject> getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate));
|
|
|
|
/// <summary>
|
|
/// Performs required tasks before every calculation.
|
|
/// </summary>
|
|
/// <param name="mods">The original list of <see cref="Mod"/>s.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
private void preProcess([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken)
|
|
{
|
|
playableMods = mods.Select(m => m.DeepClone()).ToArray();
|
|
Beatmap = WorkingBeatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
|
|
|
|
clockRate = ModUtils.CalculateRateWithMods(playableMods);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sorts a given set of <see cref="DifficultyHitObject"/>s.
|
|
/// </summary>
|
|
/// <param name="input">The <see cref="DifficultyHitObject"/>s to sort.</param>
|
|
/// <returns>The sorted <see cref="DifficultyHitObject"/>s.</returns>
|
|
protected virtual IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input)
|
|
=> input.OrderBy(h => h.BaseObject.StartTime);
|
|
|
|
/// <summary>
|
|
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
|
/// </summary>
|
|
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
|
{
|
|
return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty<Mod>()).ToArray();
|
|
|
|
static IEnumerable<Mod> createDifficultyAdjustmentModCombinations(ReadOnlyMemory<Mod> remainingMods, IEnumerable<Mod> currentSet, int currentSetCount = 0)
|
|
{
|
|
// Return the current set.
|
|
switch (currentSetCount)
|
|
{
|
|
case 0:
|
|
// Initial-case: Empty current set
|
|
yield return new ModNoMod();
|
|
|
|
break;
|
|
|
|
case 1:
|
|
yield return currentSet.Single();
|
|
|
|
break;
|
|
|
|
default:
|
|
yield return new MultiMod(currentSet.ToArray());
|
|
|
|
break;
|
|
}
|
|
|
|
// Apply the rest of the remaining mods recursively.
|
|
for (int i = 0; i < remainingMods.Length; i++)
|
|
{
|
|
(var nextSet, int nextCount) = flatten(remainingMods.Span[i]);
|
|
|
|
// Check if any mods in the next set are incompatible with any of the current set.
|
|
if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType)))
|
|
continue;
|
|
|
|
// Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves.
|
|
if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType())))
|
|
continue;
|
|
|
|
// If all's good, attach the next set to the current set and recurse further.
|
|
foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount))
|
|
yield return combo;
|
|
}
|
|
}
|
|
|
|
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
|
|
static (IEnumerable<Mod> set, int count) flatten(Mod mod)
|
|
{
|
|
if (!(mod is MultiMod multi))
|
|
return (mod.Yield(), 1);
|
|
|
|
IEnumerable<Mod> set = Enumerable.Empty<Mod>();
|
|
int count = 0;
|
|
|
|
foreach (var nested in multi.Mods)
|
|
{
|
|
(var nestedSet, int nestedCount) = flatten(nested);
|
|
set = set.Concat(nestedSet);
|
|
count += nestedCount;
|
|
}
|
|
|
|
return (set, count);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
|
/// </summary>
|
|
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
|
|
|
|
/// <summary>
|
|
/// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
|
|
/// </summary>
|
|
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.
|
|
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
|
|
/// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
|
|
/// <param name="skills">The skills which processed the beatmap.</param>
|
|
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
|
protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate);
|
|
|
|
/// <summary>
|
|
/// Enumerates <see cref="DifficultyHitObject"/>s to be processed from <see cref="HitObject"/>s in the <see cref="IBeatmap"/>.
|
|
/// </summary>
|
|
/// <param name="beatmap">The <see cref="IBeatmap"/> providing the <see cref="HitObject"/>s to enumerate.</param>
|
|
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
|
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
|
|
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
|
|
|
|
/// <summary>
|
|
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
|
|
/// </summary>
|
|
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.
|
|
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
|
|
/// <param name="mods">Mods to calculate difficulty with.</param>
|
|
/// <param name="clockRate">Clockrate to calculate difficulty with.</param>
|
|
/// <returns>The <see cref="Skill"/>s.</returns>
|
|
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate);
|
|
|
|
/// <summary>
|
|
/// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time.
|
|
/// </summary>
|
|
private class ProgressiveCalculationBeatmap : IBeatmap
|
|
{
|
|
private readonly IBeatmap baseBeatmap;
|
|
|
|
public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap)
|
|
{
|
|
this.baseBeatmap = baseBeatmap;
|
|
}
|
|
|
|
public readonly List<HitObject> HitObjects = new List<HitObject>();
|
|
|
|
IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
|
|
|
|
#region Delegated IBeatmap implementation
|
|
|
|
public BeatmapInfo BeatmapInfo
|
|
{
|
|
get => baseBeatmap.BeatmapInfo;
|
|
set => baseBeatmap.BeatmapInfo = value;
|
|
}
|
|
|
|
public ControlPointInfo ControlPointInfo
|
|
{
|
|
get => baseBeatmap.ControlPointInfo;
|
|
set => baseBeatmap.ControlPointInfo = value;
|
|
}
|
|
|
|
public BeatmapMetadata Metadata => baseBeatmap.Metadata;
|
|
|
|
public BeatmapDifficulty Difficulty
|
|
{
|
|
get => baseBeatmap.Difficulty;
|
|
set => baseBeatmap.Difficulty = value;
|
|
}
|
|
|
|
public SortedList<BreakPeriod> Breaks
|
|
{
|
|
get => baseBeatmap.Breaks;
|
|
set => baseBeatmap.Breaks = value;
|
|
}
|
|
|
|
public List<string> UnhandledEventLines => baseBeatmap.UnhandledEventLines;
|
|
|
|
public double TotalBreakTime => baseBeatmap.TotalBreakTime;
|
|
public IEnumerable<BeatmapStatistic> GetStatistics() => baseBeatmap.GetStatistics();
|
|
public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength();
|
|
public int BeatmapVersion => baseBeatmap.BeatmapVersion;
|
|
public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone());
|
|
|
|
public double AudioLeadIn
|
|
{
|
|
get => baseBeatmap.AudioLeadIn;
|
|
set => baseBeatmap.AudioLeadIn = value;
|
|
}
|
|
|
|
public float StackLeniency
|
|
{
|
|
get => baseBeatmap.StackLeniency;
|
|
set => baseBeatmap.StackLeniency = value;
|
|
}
|
|
|
|
public bool SpecialStyle
|
|
{
|
|
get => baseBeatmap.SpecialStyle;
|
|
set => baseBeatmap.SpecialStyle = value;
|
|
}
|
|
|
|
public bool LetterboxInBreaks
|
|
{
|
|
get => baseBeatmap.LetterboxInBreaks;
|
|
set => baseBeatmap.LetterboxInBreaks = value;
|
|
}
|
|
|
|
public bool WidescreenStoryboard
|
|
{
|
|
get => baseBeatmap.WidescreenStoryboard;
|
|
set => baseBeatmap.WidescreenStoryboard = value;
|
|
}
|
|
|
|
public bool EpilepsyWarning
|
|
{
|
|
get => baseBeatmap.EpilepsyWarning;
|
|
set => baseBeatmap.EpilepsyWarning = value;
|
|
}
|
|
|
|
public bool SamplesMatchPlaybackRate
|
|
{
|
|
get => baseBeatmap.SamplesMatchPlaybackRate;
|
|
set => baseBeatmap.SamplesMatchPlaybackRate = value;
|
|
}
|
|
|
|
public double DistanceSpacing
|
|
{
|
|
get => baseBeatmap.DistanceSpacing;
|
|
set => baseBeatmap.DistanceSpacing = value;
|
|
}
|
|
|
|
public int GridSize
|
|
{
|
|
get => baseBeatmap.GridSize;
|
|
set => baseBeatmap.GridSize = value;
|
|
}
|
|
|
|
public double TimelineZoom
|
|
{
|
|
get => baseBeatmap.TimelineZoom;
|
|
set => baseBeatmap.TimelineZoom = value;
|
|
}
|
|
|
|
public CountdownType Countdown
|
|
{
|
|
get => baseBeatmap.Countdown;
|
|
set => baseBeatmap.Countdown = value;
|
|
}
|
|
|
|
public int CountdownOffset
|
|
{
|
|
get => baseBeatmap.CountdownOffset;
|
|
set => baseBeatmap.CountdownOffset = value;
|
|
}
|
|
|
|
public int[] Bookmarks
|
|
{
|
|
get => baseBeatmap.Bookmarks;
|
|
set => baseBeatmap.Bookmarks = value;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
}
|