mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 23:10:50 +08:00
Change effective misscount to be based on legacy score and combo at the same time (#33066)
* 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>
This commit is contained in:
committed by
GitHub
Unverified
parent
d22b3fb200
commit
9314ea94b5
@@ -75,6 +75,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("speed_difficult_strain_count")]
|
||||
public double SpeedDifficultStrainCount { get; set; }
|
||||
|
||||
[JsonProperty("slider_nested_score_per_object")]
|
||||
public double SliderNestedScorePerObject { get; set; }
|
||||
|
||||
[JsonProperty("legacy_score_base_multiplier")]
|
||||
public double LegacyScoreBaseMultiplier { get; set; }
|
||||
|
||||
[JsonProperty("maximum_legacy_combo_score")]
|
||||
public double MaximumLegacyComboScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||
/// </summary>
|
||||
@@ -115,6 +124,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
||||
yield return (ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR, AimTopWeightedSliderFactor);
|
||||
yield return (ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR, SpeedTopWeightedSliderFactor);
|
||||
yield return (ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT, SliderNestedScorePerObject);
|
||||
yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier);
|
||||
yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -132,6 +144,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
||||
AimTopWeightedSliderFactor = values[ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR];
|
||||
SpeedTopWeightedSliderFactor = values[ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR];
|
||||
SliderNestedScorePerObject = values[ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT];
|
||||
LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER];
|
||||
MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE];
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
@@ -112,6 +113,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
? Math.Cbrt(multiplier) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||
: 0;
|
||||
|
||||
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateSliderNestedScorePerObject(beatmap, totalHits);
|
||||
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
|
||||
|
||||
var simulator = new OsuLegacyScoreSimulator();
|
||||
var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
|
||||
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
@@ -131,6 +138,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
HitCircleCount = hitCircleCount,
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
SliderNestedScorePerObject = sliderNestedScorePerObject,
|
||||
LegacyScoreBaseMultiplier = legacyScoreBaseMultiplier,
|
||||
MaximumLegacyComboScore = scoreAttributes.ComboScore
|
||||
};
|
||||
|
||||
return attributes;
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// 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 osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuLegacyScoreMissCalculator
|
||||
{
|
||||
private readonly ScoreInfo score;
|
||||
private readonly OsuDifficultyAttributes attributes;
|
||||
|
||||
public OsuLegacyScoreMissCalculator(ScoreInfo scoreInfo, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
score = scoreInfo;
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public double Calculate()
|
||||
{
|
||||
if (attributes.MaxCombo == 0 || score.LegacyTotalScore == null)
|
||||
return 0;
|
||||
|
||||
double scoreV1Multiplier = attributes.LegacyScoreBaseMultiplier * getLegacyScoreMultiplier();
|
||||
double relevantComboPerObject = calculateRelevantScoreComboPerObject();
|
||||
|
||||
double maximumMissCount = calculateMaximumComboBasedMissCount();
|
||||
|
||||
double scoreObtainedDuringMaxCombo = calculateScoreAtCombo(score.MaxCombo, relevantComboPerObject, scoreV1Multiplier);
|
||||
double remainingScore = score.LegacyTotalScore.Value - scoreObtainedDuringMaxCombo;
|
||||
|
||||
if (remainingScore <= 0)
|
||||
return maximumMissCount;
|
||||
|
||||
double remainingCombo = attributes.MaxCombo - score.MaxCombo;
|
||||
double expectedRemainingScore = calculateScoreAtCombo(remainingCombo, relevantComboPerObject, scoreV1Multiplier);
|
||||
|
||||
double scoreBasedMissCount = expectedRemainingScore / remainingScore;
|
||||
|
||||
// If there's less then one miss detected - let combo-based miss count decide if this is FC or not
|
||||
scoreBasedMissCount = Math.Max(scoreBasedMissCount, 1);
|
||||
|
||||
// Cap result by very harsh version of combo-based miss count
|
||||
return Math.Min(scoreBasedMissCount, maximumMissCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the amount of score that would be achieved at a given combo.
|
||||
/// </summary>
|
||||
private double calculateScoreAtCombo(double combo, double relevantComboPerObject, double scoreV1Multiplier)
|
||||
{
|
||||
int countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
int totalHits = countGreat + countOk + countMeh + countMiss;
|
||||
|
||||
double estimatedObjects = combo / relevantComboPerObject - 1;
|
||||
|
||||
// The combo portion of ScoreV1 follows arithmetic progression
|
||||
// Therefore, we calculate the combo portion of score using the combo per object and our current combo.
|
||||
double comboScore = relevantComboPerObject > 0 ? (2 * (relevantComboPerObject - 1) + (estimatedObjects - 1) * relevantComboPerObject) * estimatedObjects / 2 : 0;
|
||||
|
||||
// We then apply the accuracy and ScoreV1 multipliers to the resulting score.
|
||||
comboScore *= score.Accuracy * 300 / 25 * scoreV1Multiplier;
|
||||
|
||||
double objectsHit = (totalHits - countMiss) * combo / attributes.MaxCombo;
|
||||
|
||||
// Score also has a non-combo portion we need to create the final score value.
|
||||
double nonComboScore = (300 + attributes.SliderNestedScorePerObject) * score.Accuracy * objectsHit;
|
||||
|
||||
return comboScore + nonComboScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the relevant combo per object for legacy score.
|
||||
/// This assumes a uniform distribution for circles and sliders.
|
||||
/// This handles cases where objects (such as buzz sliders) do not fit a normal arithmetic progression model.
|
||||
/// </summary>
|
||||
private double calculateRelevantScoreComboPerObject()
|
||||
{
|
||||
double comboScore = attributes.MaximumLegacyComboScore;
|
||||
|
||||
// We then reverse apply the ScoreV1 multipliers to get the raw value.
|
||||
comboScore /= 300.0 / 25.0 * attributes.LegacyScoreBaseMultiplier;
|
||||
|
||||
// Reverse the arithmetic progression to work out the amount of combo per object based on the score.
|
||||
double result = (attributes.MaxCombo - 2) * attributes.MaxCombo;
|
||||
result /= Math.Max(attributes.MaxCombo + 2 * (comboScore - 1), 1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function is a harsher version of current combo-based miss count, used to provide reasonable value for cases where score-based miss count can't do this.
|
||||
/// </summary>
|
||||
private double calculateMaximumComboBasedMissCount()
|
||||
{
|
||||
int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
if (attributes.SliderCount <= 0)
|
||||
return countMiss;
|
||||
|
||||
int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
|
||||
int totalImperfectHits = countOk + countMeh + countMiss;
|
||||
|
||||
double missCount = 0;
|
||||
|
||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||
|
||||
if (score.MaxCombo < fullComboThreshold)
|
||||
missCount = Math.Pow(fullComboThreshold / Math.Max(1.0, score.MaxCombo), 2.5);
|
||||
|
||||
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
||||
missCount = Math.Min(missCount, totalImperfectHits);
|
||||
|
||||
return missCount;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Logic copied from <see cref="OsuLegacyScoreSimulator.GetLegacyScoreMultiplier"/>.
|
||||
/// </remarks>
|
||||
private double getLegacyScoreMultiplier()
|
||||
{
|
||||
bool scoreV2 = score.Mods.Any(m => m is ModScoreV2);
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in score.Mods)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case OsuModNoFail:
|
||||
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||
break;
|
||||
|
||||
case OsuModEasy:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
|
||||
case OsuModHalfTime:
|
||||
case OsuModDaycore:
|
||||
multiplier *= 0.3;
|
||||
break;
|
||||
|
||||
case OsuModHidden:
|
||||
multiplier *= 1.06;
|
||||
break;
|
||||
|
||||
case OsuModHardRock:
|
||||
multiplier *= scoreV2 ? 1.10 : 1.06;
|
||||
break;
|
||||
|
||||
case OsuModDoubleTime:
|
||||
case OsuModNightcore:
|
||||
multiplier *= scoreV2 ? 1.20 : 1.12;
|
||||
break;
|
||||
|
||||
case OsuModFlashlight:
|
||||
multiplier *= 1.12;
|
||||
break;
|
||||
|
||||
case OsuModSpunOut:
|
||||
multiplier *= 0.9;
|
||||
break;
|
||||
|
||||
case OsuModRelax:
|
||||
case OsuModAutopilot:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("speed_deviation")]
|
||||
public double? SpeedDeviation { get; set; }
|
||||
|
||||
[JsonProperty("combo_based_estimated_miss_count")]
|
||||
public double ComboBasedEstimatedMissCount { get; set; }
|
||||
|
||||
[JsonProperty("score_based_estimated_miss_count")]
|
||||
public double? ScoreBasedEstimatedMissCount { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||
{
|
||||
foreach (var attribute in base.GetAttributesForDisplay())
|
||||
|
||||
@@ -7,12 +7,12 @@ using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
@@ -95,30 +95,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
overallDifficulty = (80 - greatHitWindow) / 6;
|
||||
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
|
||||
|
||||
if (osuAttributes.SliderCount > 0)
|
||||
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
|
||||
double? scoreBasedEstimatedMissCount = null;
|
||||
|
||||
if (usingClassicSliderAccuracy && score.LegacyTotalScore != null)
|
||||
{
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount;
|
||||
var legacyScoreMissCalculator = new OsuLegacyScoreMissCalculator(score, osuAttributes);
|
||||
scoreBasedEstimatedMissCount = legacyScoreMissCalculator.Calculate();
|
||||
|
||||
if (scoreMaxCombo < fullComboThreshold)
|
||||
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
|
||||
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
||||
effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
|
||||
}
|
||||
else
|
||||
{
|
||||
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
|
||||
|
||||
if (scoreMaxCombo < fullComboThreshold)
|
||||
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
|
||||
// Combine regular misses with tick misses since tick misses break combo as well
|
||||
effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss);
|
||||
}
|
||||
effectiveMissCount = scoreBasedEstimatedMissCount.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use combo-based miss count if this isn't a legacy score
|
||||
effectiveMissCount = comboBasedEstimatedMissCount;
|
||||
}
|
||||
|
||||
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
||||
@@ -163,6 +153,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
ComboBasedEstimatedMissCount = comboBasedEstimatedMissCount,
|
||||
ScoreBasedEstimatedMissCount = scoreBasedEstimatedMissCount,
|
||||
SpeedDeviation = speedDeviation,
|
||||
Total = totalValue
|
||||
};
|
||||
@@ -325,6 +317,39 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
private double calculateComboBasedEstimatedMissCount(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (attributes.SliderCount <= 0)
|
||||
return countMiss;
|
||||
|
||||
double missCount = countMiss;
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||
|
||||
if (scoreMaxCombo < fullComboThreshold)
|
||||
missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
|
||||
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
||||
missCount = Math.Min(missCount, totalImperfectHits);
|
||||
}
|
||||
else
|
||||
{
|
||||
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
|
||||
|
||||
if (scoreMaxCombo < fullComboThreshold)
|
||||
missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
|
||||
// Combine regular misses with tick misses since tick misses break combo as well
|
||||
missCount = Math.Min(missCount, countSliderTickMiss + countMiss);
|
||||
}
|
||||
|
||||
return missCount;
|
||||
}
|
||||
|
||||
private double calculateEstimatedSliderbreaks(double topWeightedSliderFactor, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (!usingClassicSliderAccuracy || countOk == 0)
|
||||
@@ -336,6 +361,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// scores with more oks are more likely to have sliderbreaks
|
||||
double okAdjustment = ((countOk - estimatedSliderbreaks) + 0.5) / countOk;
|
||||
|
||||
// There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred.
|
||||
estimatedSliderbreaks *= DifficultyCalculationUtils.Smoothstep(effectiveMissCount, 1, 2);
|
||||
|
||||
return estimatedSliderbreaks * okAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Utils
|
||||
{
|
||||
public static class LegacyScoreUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the average amount of score per object that is caused by slider ticks.
|
||||
/// </summary>
|
||||
public static double CalculateSliderNestedScorePerObject(IBeatmap beatmap, int objectCount)
|
||||
{
|
||||
const double big_tick_score = 30;
|
||||
const double small_tick_score = 10;
|
||||
|
||||
var sliders = beatmap.HitObjects.OfType<Slider>().ToArray();
|
||||
|
||||
// 1 for head, 1 for tail
|
||||
int amountOfBigTicks = sliders.Length * 2;
|
||||
|
||||
// Add slider repeats
|
||||
amountOfBigTicks += sliders.Select(s => s.RepeatCount).Sum();
|
||||
|
||||
int amountOfSmallTicks = sliders.Select(s => s.NestedHitObjects.Count(nho => nho is SliderTick)).Sum();
|
||||
|
||||
double totalScore = amountOfBigTicks * big_tick_score + amountOfSmallTicks * small_tick_score;
|
||||
|
||||
return totalScore / objectCount;
|
||||
}
|
||||
|
||||
public static int CalculateDifficultyPeppyStars(IBeatmap beatmap)
|
||||
{
|
||||
int objectCount = beatmap.HitObjects.Count;
|
||||
int drainLength = 0;
|
||||
|
||||
if (objectCount > 0)
|
||||
{
|
||||
int breakLength = beatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||
drainLength = ((int)Math.Round(beatmap.HitObjects[^1].StartTime) - (int)Math.Round(beatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
return LegacyRulesetExtensions.CalculateDifficultyPeppyStars(beatmap.Difficulty, objectCount, drainLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
|
||||
protected const int ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR = 33;
|
||||
protected const int ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR = 35;
|
||||
protected const int ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT = 37;
|
||||
protected const int ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER = 39;
|
||||
protected const int ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE = 41;
|
||||
|
||||
/// <summary>
|
||||
/// The mods which were applied to the beatmap.
|
||||
|
||||
@@ -28,11 +28,15 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// </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;
|
||||
private readonly IWorkingBeatmap beatmap;
|
||||
|
||||
/// <summary>
|
||||
/// A yymmdd version which is used to discern when reprocessing is required.
|
||||
@@ -42,7 +46,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
{
|
||||
this.ruleset = ruleset;
|
||||
this.beatmap = beatmap;
|
||||
WorkingBeatmap = beatmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -178,7 +182,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
private void preProcess([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken)
|
||||
{
|
||||
playableMods = mods.Select(m => m.DeepClone()).ToArray();
|
||||
Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
|
||||
Beatmap = WorkingBeatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
|
||||
|
||||
clockRate = ModUtils.CalculateRateWithMods(playableMods);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user