Restructure PerformanceCalculator to not require ScoreInfo argument

This commit is contained in:
Dan Balasescu
2022-03-14 14:25:26 +09:00
parent fa456da0ec
commit 4a3e3aba65
14 changed files with 147 additions and 189 deletions

View File

@ -19,7 +19,6 @@ using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System; using System;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Catch.Edit; using osu.Game.Rulesets.Catch.Edit;
@ -182,7 +181,7 @@ namespace osu.Game.Rulesets.Catch
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin); public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new CatchPerformanceCalculator(this);
public int LegacyID => 2; public int LegacyID => 2;

View File

@ -13,33 +13,29 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public class CatchPerformanceCalculator : PerformanceCalculator public class CatchPerformanceCalculator : PerformanceCalculator
{ {
protected new CatchDifficultyAttributes Attributes => (CatchDifficultyAttributes)base.Attributes;
private Mod[] mods;
private int fruitsHit; private int fruitsHit;
private int ticksHit; private int ticksHit;
private int tinyTicksHit; private int tinyTicksHit;
private int tinyTicksMissed; private int tinyTicksMissed;
private int misses; private int misses;
public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public CatchPerformanceCalculator(Ruleset ruleset)
: base(ruleset, attributes, score) : base(ruleset)
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var catchAttributes = (CatchDifficultyAttributes)attributes;
fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great); fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit); ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit); tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss); tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
misses = Score.Statistics.GetValueOrDefault(HitResult.Miss); misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
// We are heavily relying on aim in catch the beat // We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0; double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo // Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits(); int numTotalHits = totalComboHits();
@ -52,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= Math.Pow(0.97, misses); value *= Math.Pow(0.97, misses);
// Combo scaling // Combo scaling
if (Attributes.MaxCombo > 0) if (catchAttributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
double approachRate = Attributes.ApproachRate; double approachRate = catchAttributes.ApproachRate;
double approachRateFactor = 1.0; double approachRateFactor = 1.0;
if (approachRate > 9.0) if (approachRate > 9.0)
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
@ -66,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= approachRateFactor; value *= approachRateFactor;
if (mods.Any(m => m is ModHidden)) if (score.Mods.Any(m => m is ModHidden))
{ {
// Hiddens gives almost nothing on max approach rate, and more the lower it is // Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0) if (approachRate <= 10.0)
@ -75,12 +71,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11 value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11
} }
if (mods.Any(m => m is ModFlashlight)) if (score.Mods.Any(m => m is ModFlashlight))
value *= 1.35 * lengthBonus; value *= 1.35 * lengthBonus;
value *= Math.Pow(accuracy(), 5.5); value *= Math.Pow(accuracy(), 5.5);
if (mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
value *= 0.90; value *= 0.90;
return new CatchPerformanceAttributes return new CatchPerformanceAttributes

View File

@ -13,10 +13,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
public class ManiaPerformanceCalculator : PerformanceCalculator public class ManiaPerformanceCalculator : PerformanceCalculator
{ {
protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes;
private Mod[] mods;
// Score after being scaled by non-difficulty-increasing mods // Score after being scaled by non-difficulty-increasing mods
private double scaledScore; private double scaledScore;
@ -27,39 +23,40 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public ManiaPerformanceCalculator(Ruleset ruleset)
: base(ruleset, attributes, score) : base(ruleset)
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var maniaAttributes = (ManiaDifficultyAttributes)attributes;
scaledScore = Score.TotalScore;
countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = Score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
if (Attributes.ScoreMultiplier > 0) scaledScore = score.TotalScore;
countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
if (maniaAttributes.ScoreMultiplier > 0)
{ {
// Scale score up, so it's comparable to other keymods // Scale score up, so it's comparable to other keymods
scaledScore *= 1.0 / Attributes.ScoreMultiplier; scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
} }
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed. // The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8; double multiplier = 0.8;
if (mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.9; multiplier *= 0.9;
if (mods.Any(m => m is ModEasy)) if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.5; multiplier *= 0.5;
double difficultyValue = computeDifficultyValue(); double difficultyValue = computeDifficultyValue(maniaAttributes);
double accValue = computeAccuracyValue(difficultyValue); double accValue = computeAccuracyValue(difficultyValue, maniaAttributes);
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(difficultyValue, 1.1) + Math.Pow(difficultyValue, 1.1) +
@ -75,9 +72,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
}; };
} }
private double computeDifficultyValue() private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
{ {
double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
@ -97,14 +94,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return difficultyValue; return difficultyValue;
} }
private double computeAccuracyValue(double difficultyValue) private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
{ {
if (Attributes.GreatHitWindow <= 0) if (attributes.GreatHitWindow <= 0)
return 0; return 0;
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667) double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)
* difficultyValue * difficultyValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new ManiaPerformanceCalculator(this);
public const string SHORT_NAME = "mania"; public const string SHORT_NAME = "mania";

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -14,10 +13,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuPerformanceCalculator : PerformanceCalculator public class OsuPerformanceCalculator : PerformanceCalculator
{ {
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
private Mod[] mods;
private double accuracy; private double accuracy;
private int scoreMaxCombo; private int scoreMaxCombo;
private int countGreat; private int countGreat;
@ -27,31 +22,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double effectiveMissCount; private double effectiveMissCount;
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public OsuPerformanceCalculator(Ruleset ruleset)
: base(ruleset, attributes, score) : base(ruleset)
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var osuAttributes = (OsuDifficultyAttributes)attributes;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo; accuracy = score.Accuracy;
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great); scoreMaxCombo = score.MaxCombo;
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
effectiveMissCount = calculateEffectiveMissCount(); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
if (mods.Any(m => m is OsuModNoFail)) if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0) if (score.Mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); multiplier *= 1.0 - Math.Pow((double)osuAttributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
{ {
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
@ -59,10 +55,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
multiplier *= 0.6; multiplier *= 0.6;
} }
double aimValue = computeAimValue(); double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(); double speedValue = computeSpeedValue(score, osuAttributes);
double accuracyValue = computeAccuracyValue(); double accuracyValue = computeAccuracyValue(score, osuAttributes);
double flashlightValue = computeFlashlightValue(); double flashlightValue = computeFlashlightValue(score, osuAttributes);
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(aimValue, 1.1) + Math.Pow(aimValue, 1.1) +
@ -82,11 +78,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}; };
} }
private double computeAimValue() private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
double rawAim = Attributes.AimDifficulty; double rawAim = attributes.AimDifficulty;
if (mods.Any(m => m is OsuModTouchDevice)) if (score.Mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8); rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0; double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
@ -99,44 +95,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount); aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
aimValue *= getComboScalingFactor(); aimValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0) else if (attributes.ApproachRate < 8.0)
approachRateFactor = 0.1 * (8.0 - Attributes.ApproachRate); approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate);
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModBlinds)) if (score.Mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
else if (mods.Any(h => h is OsuModHidden)) else if (score.Mods.Any(h => h is OsuModHidden))
{ {
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
} }
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
double estimateDifficultSliders = Attributes.SliderCount * 0.15; double estimateDifficultSliders = attributes.SliderCount * 0.15;
if (Attributes.SliderCount > 0) if (attributes.SliderCount > 0)
{ {
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, Attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
double sliderNerfFactor = (1 - Attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + Attributes.SliderFactor; double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor; aimValue *= sliderNerfFactor;
} }
aimValue *= accuracy; aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy. // It is important to consider accuracy difficulty when scaling with accuracy.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
return aimValue; return aimValue;
} }
private double computeSpeedValue() private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -146,27 +142,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
speedValue *= getComboScalingFactor(); speedValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModBlinds)) if (score.Mods.Any(m => m is OsuModBlinds))
{ {
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12; speedValue *= 1.12;
} }
else if (mods.Any(m => m is OsuModHidden)) else if (score.Mods.Any(m => m is OsuModHidden))
{ {
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
} }
// Scale the speed value with accuracy and OD. // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@ -174,14 +170,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return speedValue; return speedValue;
} }
private double computeAccuracyValue() private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
if (mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
return 0.0; return 0.0;
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage; double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
if (amountHitObjectsWithAccuracy > 0) if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
@ -194,43 +190,43 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer. // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (mods.Any(m => m is OsuModBlinds)) if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14; accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden)) else if (score.Mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08; accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight)) if (score.Mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02; accuracyValue *= 1.02;
return accuracyValue; return accuracyValue;
} }
private double computeFlashlightValue() private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
if (!mods.Any(h => h is OsuModFlashlight)) if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0; return 0.0;
double rawFlashlight = Attributes.FlashlightDifficulty; double rawFlashlight = attributes.FlashlightDifficulty;
if (mods.Any(m => m is OsuModTouchDevice)) if (score.Mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8); rawFlashlight = Math.Pow(rawFlashlight, 0.8);
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0; double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
if (mods.Any(h => h is OsuModHidden)) if (score.Mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3; flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
flashlightValue *= getComboScalingFactor(); flashlightValue *= getComboScalingFactor(attributes);
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
@ -239,19 +235,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the flashlight value with accuracy _slightly_. // Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0; flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that. // It is important to also consider accuracy difficulty when doing that.
flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
return flashlightValue; return flashlightValue;
} }
private double calculateEffectiveMissCount() private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
{ {
// Guess the number of misses + slider breaks from combo // Guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0; double comboBasedMissCount = 0.0;
if (Attributes.SliderCount > 0) if (attributes.SliderCount > 0)
{ {
double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount; double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold) if (scoreMaxCombo < fullComboThreshold)
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
} }
@ -262,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Max(countMiss, comboBasedMissCount); return Math.Max(countMiss, comboBasedMissCount);
} }
private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalSuccessfulHits => countGreat + countOk + countMeh;
} }

View File

@ -213,7 +213,7 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap); public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new OsuPerformanceCalculator(this);
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);

View File

@ -14,37 +14,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
public class TaikoPerformanceCalculator : PerformanceCalculator public class TaikoPerformanceCalculator : PerformanceCalculator
{ {
protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes;
private Mod[] mods;
private int countGreat; private int countGreat;
private int countOk; private int countOk;
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public TaikoPerformanceCalculator(Ruleset ruleset)
: base(ruleset, attributes, score) : base(ruleset)
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var taikoAttributes = (TaikoDifficultyAttributes)attributes;
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.90; multiplier *= 0.90;
if (mods.Any(m => m is ModHidden)) if (score.Mods.Any(m => m is ModHidden))
multiplier *= 1.10; multiplier *= 1.10;
double difficultyValue = computeDifficultyValue(); double difficultyValue = computeDifficultyValue(score, taikoAttributes);
double accuracyValue = computeAccuracyValue(); double accuracyValue = computeAccuracyValue(score, taikoAttributes);
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(difficultyValue, 1.1) + Math.Pow(difficultyValue, 1.1) +
@ -59,30 +57,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
}; };
} }
private double computeDifficultyValue() private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{ {
double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus; difficultyValue *= lengthBonus;
difficultyValue *= Math.Pow(0.985, countMiss); difficultyValue *= Math.Pow(0.985, countMiss);
if (mods.Any(m => m is ModHidden)) if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025; difficultyValue *= 1.025;
if (mods.Any(m => m is ModFlashlight<TaikoHitObject>)) if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.05 * lengthBonus; difficultyValue *= 1.05 * lengthBonus;
return difficultyValue * Score.Accuracy; return difficultyValue * score.Accuracy;
} }
private double computeAccuracyValue() private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{ {
if (Attributes.GreatHitWindow <= 0) if (attributes.GreatHitWindow <= 0)
return 0; return 0;
double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0; double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0;
// Bonus for many objects - it's harder to keep good accuracy up for longer // Bonus for many objects - it's harder to keep good accuracy up for longer
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));

View File

@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap); public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator(this);
public int LegacyID => 1; public int LegacyID => 1;

View File

@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.Ranking
private class RulesetWithNoPerformanceCalculator : OsuRuleset private class RulesetWithNoPerformanceCalculator : OsuRuleset
{ {
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; public override PerformanceCalculator CreatePerformanceCalculator() => null;
} }
} }
} }

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Difficulty
).ConfigureAwait(false); ).ConfigureAwait(false);
// ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes
return difficulty == null ? null : ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes);
}, cancellationToken); }, cancellationToken);
} }

View File

@ -1,41 +1,31 @@
// 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.
using System; using osu.Game.Beatmaps;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Rulesets.Difficulty namespace osu.Game.Rulesets.Difficulty
{ {
public abstract class PerformanceCalculator public abstract class PerformanceCalculator
{ {
protected readonly DifficultyAttributes Attributes;
protected readonly Ruleset Ruleset; protected readonly Ruleset Ruleset;
protected readonly ScoreInfo Score;
protected double TimeRate { get; private set; } = 1; protected PerformanceCalculator(Ruleset ruleset)
protected PerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
{ {
Ruleset = ruleset; Ruleset = ruleset;
Score = score;
Attributes = attributes ?? throw new ArgumentNullException(nameof(attributes));
ApplyMods(score.Mods);
} }
protected virtual void ApplyMods(Mod[] mods) public PerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes)
{ => CreatePerformanceAttributes(score, attributes);
var track = new TrackVirtual(10000);
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
TimeRate = track.Rate;
}
public abstract PerformanceAttributes Calculate(); public PerformanceAttributes Calculate(ScoreInfo score, IWorkingBeatmap beatmap)
=> Calculate(score, Ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods));
/// <summary>
/// Creates <see cref="PerformanceAttributes"/> to describe a score's performance.
/// </summary>
/// <param name="score">The score to create the attributes for.</param>
/// <param name="attributes">The difficulty attributes for the beatmap relating to the score.</param>
protected abstract PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes);
} }
} }

View File

@ -228,25 +228,9 @@ namespace osu.Game.Rulesets
/// <summary> /// <summary>
/// Optionally creates a <see cref="PerformanceCalculator"/> to generate performance data from the provided score. /// Optionally creates a <see cref="PerformanceCalculator"/> to generate performance data from the provided score.
/// </summary> /// </summary>
/// <param name="attributes">Difficulty attributes for the beatmap related to the provided score.</param>
/// <param name="score">The score to be processed.</param>
/// <returns>A performance calculator instance for the provided score.</returns> /// <returns>A performance calculator instance for the provided score.</returns>
[CanBeNull] [CanBeNull]
public virtual PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; public virtual PerformanceCalculator CreatePerformanceCalculator() => null;
/// <summary>
/// Optionally creates a <see cref="PerformanceCalculator"/> to generate performance data from the provided score.
/// </summary>
/// <param name="beatmap">The beatmap to use as a source for generating <see cref="DifficultyAttributes"/>.</param>
/// <param name="score">The score to be processed.</param>
/// <returns>A performance calculator instance for the provided score.</returns>
[CanBeNull]
public PerformanceCalculator CreatePerformanceCalculator(IWorkingBeatmap beatmap, ScoreInfo score)
{
var difficultyCalculator = CreateDifficultyCalculator(beatmap);
var difficultyAttributes = difficultyCalculator.Calculate(score.Mods);
return CreatePerformanceCalculator(difficultyAttributes, score);
}
public virtual HitObjectComposer CreateHitObjectComposer() => null; public virtual HitObjectComposer CreateHitObjectComposer() => null;

View File

@ -43,9 +43,7 @@ namespace osu.Game.Scoring
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score); return score.Ruleset.CreateInstance().CreatePerformanceCalculator()?.Calculate(score, attributes.Value.Attributes);
return calculator?.Calculate();
} }
public readonly struct PerformanceCacheLookup public readonly struct PerformanceCacheLookup

View File

@ -130,9 +130,9 @@ namespace osu.Game.Screens.Play.HUD
var scoreInfo = gameplayState.Score.ScoreInfo.DeepClone(); var scoreInfo = gameplayState.Score.ScoreInfo.DeepClone();
scoreInfo.Mods = clonedMods; scoreInfo.Mods = clonedMods;
var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, scoreInfo); var calculator = gameplayState.Ruleset.CreatePerformanceCalculator();
Current.Value = (int)Math.Round(calculator?.Calculate().Total ?? 0, MidpointRounding.AwayFromZero); Current.Value = (int)Math.Round(calculator?.Calculate(scoreInfo, attrib).Total ?? 0, MidpointRounding.AwayFromZero);
IsValid = true; IsValid = true;
} }