diff --git a/PerformanceCalculator/Simulate/OsuSimulateCommand.cs b/PerformanceCalculator/Simulate/OsuSimulateCommand.cs index 81079e8d33..504a2b1ba8 100644 --- a/PerformanceCalculator/Simulate/OsuSimulateCommand.cs +++ b/PerformanceCalculator/Simulate/OsuSimulateCommand.cs @@ -50,32 +50,63 @@ protected override Dictionary GenerateHitResults(IBeatmap beatma // Use lazer info only if score has sliderhead accuracy if (mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value)) { - return generateHitResults(beatmap, Accuracy / 100, Misses, Mehs, Goods, null, null); + return generateHitResults(beatmap, Accuracy / 100, mods, Misses, Mehs, Goods, null, null); } else { - return generateHitResults(beatmap, Accuracy / 100, Misses, Mehs, Goods, largeTickMisses, sliderTailMisses); + return generateHitResults(beatmap, Accuracy / 100, mods, Misses, Mehs, Goods, largeTickMisses, sliderTailMisses); } } - private static Dictionary generateHitResults(IBeatmap beatmap, double accuracy, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses) + private static Dictionary generateHitResults(IBeatmap beatmap, double accuracy, Mod[] mods, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses) { + bool usingClassicSliderAccuracy = mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value); + int countGreat; int totalResultCount = beatmap.HitObjects.Count; + int countLargeTicks = beatmap.HitObjects.Sum(obj => obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat)); + int countSmallTicks = beatmap.HitObjects.Count(x => x is Slider); + + // Sliderheads are large ticks too if slideracc is disabled + if (usingClassicSliderAccuracy) + countLargeTicks += countSmallTicks; + + countLargeTickMisses = Math.Min(countLargeTickMisses ?? 0, countLargeTicks); + countSliderTailMisses = Math.Min(countSliderTailMisses ?? 0, countSmallTicks); + if (countMeh != null || countGood != null) { countGreat = totalResultCount - (countGood ?? 0) - (countMeh ?? 0) - countMiss; } else { - // Total result count excluding countMiss - int relevantResultCount = totalResultCount - countMiss; + // Relevant result count without misses (normal misses and slider-related misses) + // We need to exclude them from judgement count so total value will be equal to desired after misses are accounted for + double countSuccessfulHits; + + // If there's no classic slider accuracy - we need to weight normal judgements accordingly. + // Normal judgements in this context are 300s, 100s, 50s and misses. + // Slider-related judgements are large tick hits/misses and slider tail hits/misses. + double normalJudgementWeight = 1.0; + + if (usingClassicSliderAccuracy) + { + countSuccessfulHits = totalResultCount - countMiss; + } + else + { + double maxSliderPortion = countSmallTicks * 0.5 + countLargeTicks * 0.1; + normalJudgementWeight = (totalResultCount + maxSliderPortion) / totalResultCount; + + double missedSliderPortion = (double)countSliderTailMisses * 0.5 + (double)countLargeTickMisses * 0.1; + countSuccessfulHits = totalResultCount - (countMiss + missedSliderPortion) / normalJudgementWeight; + } // Accuracy excluding countMiss. We need that because we're trying to achieve target accuracy without touching countMiss // So it's better to pretened that there were 0 misses in the 1st place - double relevantAccuracy = accuracy * totalResultCount / relevantResultCount; + double relevantAccuracy = accuracy * totalResultCount / countSuccessfulHits; // Clamp accuracy to account for user trying to break the algorithm by inputting impossible values relevantAccuracy = Math.Clamp(relevantAccuracy, 0, 1); @@ -87,7 +118,7 @@ private static Dictionary generateHitResults(IBeatmap beatmap, d double ratio50To100 = Math.Pow(1 - (relevantAccuracy - 0.25) / 0.75, 2); // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c50 = c100 * ratio50to100 - double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4); + double count100Estimate = 6 * countSuccessfulHits * (1 - relevantAccuracy) / (5 * ratio50To100 + 4) * normalJudgementWeight; // Get count50 according to c50 = c100 * ratio50to100 double count50Estimate = count100Estimate * ratio50To100; @@ -102,23 +133,23 @@ private static Dictionary generateHitResults(IBeatmap beatmap, d else if (relevantAccuracy >= 1.0 / 6) { // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = 0 - double count100Estimate = 6 * relevantResultCount * relevantAccuracy - relevantResultCount; + double count100Estimate = 6 * countSuccessfulHits * relevantAccuracy - countSuccessfulHits; // We only had 100s and 50s in that scenario so rest of the hits are 50s - double count50Estimate = relevantResultCount - count100Estimate; + double count50Estimate = countSuccessfulHits - count100Estimate; // Round it to get int number of 100s - countGood = (int?)Math.Round(count100Estimate); + countGood = (int?)Math.Round(count100Estimate * normalJudgementWeight); // Get number of 50s as difference between total mistimed hits and count100 - countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood); + countMeh = (int?)(Math.Round((count100Estimate + count50Estimate) * normalJudgementWeight) - countGood); } // If accuracy is less than 16.67% - it means that we have only 50s or misses // Assuming that we removed misses in the 1st place - that means that we need to add additional misses to achieve target accuracy else { // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = c100 = 0 - double count50Estimate = 6 * relevantResultCount * relevantAccuracy; + double count50Estimate = 6 * (totalResultCount - countMiss) * relevantAccuracy; // We have 0 100s, because we can't start adding 100s again after reaching "only 50s" point countGood = 0; @@ -130,6 +161,10 @@ private static Dictionary generateHitResults(IBeatmap beatmap, d countMiss = (int)(totalResultCount - countMeh); } + // Clamp goods if total amount is bigger than possible + countGood -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countGood); + countMeh -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countMeh); + // Rest of the hits are 300s countGreat = (int)(totalResultCount - countGood - countMeh - countMiss); } @@ -142,17 +177,18 @@ private static Dictionary generateHitResults(IBeatmap beatmap, d { HitResult.Miss, countMiss } }; - if (countLargeTickMisses != null) - result[HitResult.LargeTickMiss] = countLargeTickMisses.Value; - - if (countSliderTailMisses != null) - result[HitResult.SliderTailHit] = beatmap.HitObjects.Count(x => x is Slider) - countSliderTailMisses.Value; + result[HitResult.LargeTickHit] = countLargeTicks - (int)countLargeTickMisses; + result[HitResult.LargeTickMiss] = (int)countLargeTickMisses; + result[usingClassicSliderAccuracy ? HitResult.SmallTickHit : HitResult.SliderTailHit] = countSmallTicks - (int)countSliderTailMisses; + if (usingClassicSliderAccuracy) result[HitResult.SmallTickMiss] = (int)countSliderTailMisses; return result; } protected override double GetAccuracy(IBeatmap beatmap, Dictionary statistics, Mod[] mods) { + bool usingClassicSliderAccuracy = mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value); + int countGreat = statistics[HitResult.Great]; int countGood = statistics[HitResult.Ok]; int countMeh = statistics[HitResult.Meh]; @@ -161,7 +197,7 @@ protected override double GetAccuracy(IBeatmap beatmap, Dictionary x is Slider); @@ -169,10 +205,10 @@ protected override double GetAccuracy(IBeatmap beatmap, Dictionary obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat)); - int countLargeTickHit = countLargeTicks - countLargeTickMiss; + int countLargeTickHit = countLargeTicks - countLargeTicksMiss; total += 0.6 * countLargeTickHit; max += 0.6 * countLargeTicks; diff --git a/PerformanceCalculatorGUI/RulesetHelper.cs b/PerformanceCalculatorGUI/RulesetHelper.cs index 6220b7a6d5..9e8f40044a 100644 --- a/PerformanceCalculatorGUI/RulesetHelper.cs +++ b/PerformanceCalculatorGUI/RulesetHelper.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; @@ -66,7 +67,7 @@ public static Dictionary GenerateHitResultsForRuleset(RulesetInf { return ruleset.OnlineID switch { - 0 => generateOsuHitResults(accuracy, beatmap, countMiss, countMeh, countGood, countLargeTickMisses, countSliderTailMisses), + 0 => generateOsuHitResults(accuracy, beatmap, mods, countMiss, countMeh, countGood, countLargeTickMisses, countSliderTailMisses), 1 => generateTaikoHitResults(accuracy, beatmap, countMiss, countGood), 2 => generateCatchHitResults(accuracy, beatmap, countMiss, countMeh, countGood), 3 => generateManiaHitResults(accuracy, beatmap, mods, countMiss), @@ -74,24 +75,55 @@ public static Dictionary GenerateHitResultsForRuleset(RulesetInf }; } - private static Dictionary generateOsuHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses) + private static Dictionary generateOsuHitResults(double accuracy, IBeatmap beatmap, Mod[] mods, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses) { + bool usingClassicSliderAccuracy = mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value); + int countGreat; int totalResultCount = beatmap.HitObjects.Count; + int countLargeTicks = beatmap.HitObjects.Sum(obj => obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat)); + int countSmallTicks = beatmap.HitObjects.Count(x => x is Slider); + + // Sliderheads are large ticks too if slideracc is disabled + if (usingClassicSliderAccuracy) + countLargeTicks += countSmallTicks; + + countLargeTickMisses = Math.Min(countLargeTickMisses ?? 0, countLargeTicks); + countSliderTailMisses = Math.Min(countSliderTailMisses ?? 0, countSmallTicks); + if (countMeh != null || countGood != null) { countGreat = totalResultCount - (countGood ?? 0) - (countMeh ?? 0) - countMiss; } else { - // Total result count excluding countMiss - int relevantResultCount = totalResultCount - countMiss; + // Relevant result count without misses (normal misses and slider-related misses) + // We need to exclude them from judgement count so total value will be equal to desired after misses are accounted for + double countSuccessfulHits; + + // If there's no classic slider accuracy - we need to weight normal judgements accordingly. + // Normal judgements in this context are 300s, 100s, 50s and misses. + // Slider-related judgements are large tick hits/misses and slider tail hits/misses. + double normalJudgementWeight = 1.0; + + if (usingClassicSliderAccuracy) + { + countSuccessfulHits = totalResultCount - countMiss; + } + else + { + double maxSliderPortion = countSmallTicks * 0.5 + countLargeTicks * 0.1; + normalJudgementWeight = (totalResultCount + maxSliderPortion) / totalResultCount; + + double missedSliderPortion = (double)countSliderTailMisses * 0.5 + (double)countLargeTickMisses * 0.1; + countSuccessfulHits = totalResultCount - (countMiss + missedSliderPortion) / normalJudgementWeight; + } // Accuracy excluding countMiss. We need that because we're trying to achieve target accuracy without touching countMiss // So it's better to pretened that there were 0 misses in the 1st place - double relevantAccuracy = accuracy * totalResultCount / relevantResultCount; + double relevantAccuracy = accuracy * totalResultCount / countSuccessfulHits; // Clamp accuracy to account for user trying to break the algorithm by inputting impossible values relevantAccuracy = Math.Clamp(relevantAccuracy, 0, 1); @@ -103,7 +135,7 @@ private static Dictionary generateOsuHitResults(double accuracy, double ratio50To100 = Math.Pow(1 - (relevantAccuracy - 0.25) / 0.75, 2); // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c50 = c100 * ratio50to100 - double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4); + double count100Estimate = 6 * countSuccessfulHits * (1 - relevantAccuracy) / (5 * ratio50To100 + 4) * normalJudgementWeight; // Get count50 according to c50 = c100 * ratio50to100 double count50Estimate = count100Estimate * ratio50To100; @@ -118,23 +150,23 @@ private static Dictionary generateOsuHitResults(double accuracy, else if (relevantAccuracy >= 1.0 / 6) { // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = 0 - double count100Estimate = 6 * relevantResultCount * relevantAccuracy - relevantResultCount; + double count100Estimate = 6 * countSuccessfulHits * relevantAccuracy - countSuccessfulHits; // We only had 100s and 50s in that scenario so rest of the hits are 50s - double count50Estimate = relevantResultCount - count100Estimate; + double count50Estimate = countSuccessfulHits - count100Estimate; // Round it to get int number of 100s - countGood = (int?)Math.Round(count100Estimate); + countGood = (int?)Math.Round(count100Estimate * normalJudgementWeight); // Get number of 50s as difference between total mistimed hits and count100 - countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood); + countMeh = (int?)(Math.Round((count100Estimate + count50Estimate) * normalJudgementWeight) - countGood); } // If accuracy is less than 16.67% - it means that we have only 50s or misses // Assuming that we removed misses in the 1st place - that means that we need to add additional misses to achieve target accuracy else { // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = c100 = 0 - double count50Estimate = 6 * relevantResultCount * relevantAccuracy; + double count50Estimate = 6 * (totalResultCount - countMiss) * relevantAccuracy; // We have 0 100s, because we can't start adding 100s again after reaching "only 50s" point countGood = 0; @@ -146,6 +178,10 @@ private static Dictionary generateOsuHitResults(double accuracy, countMiss = (int)(totalResultCount - countMeh); } + // Clamp goods if total amount is bigger than possible + countGood -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countGood); + countMeh -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countMeh); + // Rest of the hits are 300s countGreat = (int)(totalResultCount - countGood - countMeh - countMiss); } @@ -158,11 +194,10 @@ private static Dictionary generateOsuHitResults(double accuracy, { HitResult.Miss, countMiss } }; - if (countLargeTickMisses != null) - result[HitResult.LargeTickMiss] = countLargeTickMisses.Value; - - if (countSliderTailMisses != null) - result[HitResult.SliderTailHit] = beatmap.HitObjects.Count(x => x is Slider) - countSliderTailMisses.Value; + result[HitResult.LargeTickHit] = countLargeTicks - (int)countLargeTickMisses; + result[HitResult.LargeTickMiss] = (int)countLargeTickMisses; + result[usingClassicSliderAccuracy ? HitResult.SmallTickHit : HitResult.SliderTailHit] = countSmallTicks - (int)countSliderTailMisses; + if (usingClassicSliderAccuracy) result[HitResult.SmallTickMiss] = (int)countSliderTailMisses; return result; } @@ -279,7 +314,7 @@ public static double GetAccuracyForRuleset(RulesetInfo ruleset, IBeatmap beatmap { return ruleset.OnlineID switch { - 0 => getOsuAccuracy(beatmap, statistics), + 0 => getOsuAccuracy(beatmap, statistics, mods), 1 => getTaikoAccuracy(statistics), 2 => getCatchAccuracy(statistics), 3 => getManiaAccuracy(statistics, mods), @@ -287,8 +322,10 @@ public static double GetAccuracyForRuleset(RulesetInfo ruleset, IBeatmap beatmap }; } - private static double getOsuAccuracy(IBeatmap beatmap, Dictionary statistics) + private static double getOsuAccuracy(IBeatmap beatmap, Dictionary statistics, Mod[] mods) { + bool usingClassicSliderAccuracy = mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value); + int countGreat = statistics[HitResult.Great]; int countGood = statistics[HitResult.Ok]; int countMeh = statistics[HitResult.Meh]; @@ -297,7 +334,7 @@ private static double getOsuAccuracy(IBeatmap beatmap, Dictionary x is Slider); @@ -305,7 +342,7 @@ private static double getOsuAccuracy(IBeatmap beatmap, Dictionary obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat)); int countLargeTickHit = countLargeTicks - countLargeTicksMiss;