diff --git a/Difficalcy.Catch/Services/CatchCalculatorService.cs b/Difficalcy.Catch/Services/CatchCalculatorService.cs index 0bb008c..2b11f50 100644 --- a/Difficalcy.Catch/Services/CatchCalculatorService.cs +++ b/Difficalcy.Catch/Services/CatchCalculatorService.cs @@ -8,7 +8,6 @@ using Difficalcy.Models; using Difficalcy.Services; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Legacy; using osu.Game.Online.API; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Difficulty; @@ -88,6 +87,24 @@ object difficultyAttributes { var catchDifficultyAttributes = (CatchDifficultyAttributes)difficultyAttributes; + var scoreInfo = GetScoreInfo(score); + + var performanceCalculator = CatchRuleset.CreatePerformanceCalculator(); + var performanceAttributes = + performanceCalculator.Calculate(scoreInfo, catchDifficultyAttributes) + as CatchPerformanceAttributes; + + return new CatchCalculation() + { + Difficulty = GetDifficultyFromDifficultyAttributes(catchDifficultyAttributes), + Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), + Accuracy = scoreInfo.Accuracy, + Combo = scoreInfo.MaxCombo, + }; + } + + private ScoreInfo GetScoreInfo(CatchScore score) + { var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(CatchRuleset.RulesetInfo, mods); @@ -98,7 +115,7 @@ object difficultyAttributes + beatmap .HitObjects.OfType() .SelectMany(j => j.NestedHitObjects) - .Count(h => !(h is TinyDroplet)); + .Count(h => h is not TinyDroplet); var statistics = GetHitResults( beatmap, score.Misses, @@ -107,26 +124,13 @@ object difficultyAttributes ); var accuracy = CalculateAccuracy(statistics); - var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, CatchRuleset.RulesetInfo) + return new ScoreInfo(beatmap.BeatmapInfo, CatchRuleset.RulesetInfo) { Accuracy = accuracy, MaxCombo = combo, Statistics = statistics, Mods = mods, }; - - var performanceCalculator = CatchRuleset.CreatePerformanceCalculator(); - var performanceAttributes = - performanceCalculator.Calculate(scoreInfo, catchDifficultyAttributes) - as CatchPerformanceAttributes; - - return new CatchCalculation() - { - Difficulty = GetDifficultyFromDifficultyAttributes(catchDifficultyAttributes), - Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), - Accuracy = accuracy, - Combo = combo, - }; } private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId) diff --git a/Difficalcy.Mania/Services/ManiaCalculatorService.cs b/Difficalcy.Mania/Services/ManiaCalculatorService.cs index 0a30b93..956276c 100644 --- a/Difficalcy.Mania/Services/ManiaCalculatorService.cs +++ b/Difficalcy.Mania/Services/ManiaCalculatorService.cs @@ -7,7 +7,6 @@ using Difficalcy.Mania.Models; using Difficalcy.Models; using Difficalcy.Services; -using osu.Game.Beatmaps.Legacy; using osu.Game.Online.API; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Difficulty; @@ -87,6 +86,24 @@ object difficultyAttributes ) { var maniaDifficultyAttributes = (ManiaDifficultyAttributes)difficultyAttributes; + + var scoreInfo = GetScoreInfo(score); + + var performanceCalculator = ManiaRuleset.CreatePerformanceCalculator(); + var performanceAttributes = + performanceCalculator.Calculate(scoreInfo, maniaDifficultyAttributes) + as ManiaPerformanceAttributes; + + return new ManiaCalculation() + { + Difficulty = GetDifficultyFromDifficultyAttributes(maniaDifficultyAttributes), + Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), + Accuracy = scoreInfo.Accuracy, + }; + } + + private ScoreInfo GetScoreInfo(ManiaScore score) + { var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(ManiaRuleset.RulesetInfo, mods); @@ -103,25 +120,13 @@ object difficultyAttributes ); var accuracy = CalculateAccuracy(statistics); - var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, ManiaRuleset.RulesetInfo) + return new ScoreInfo(beatmap.BeatmapInfo, ManiaRuleset.RulesetInfo) { Accuracy = accuracy, MaxCombo = 0, Statistics = statistics, Mods = mods, }; - - var performanceCalculator = ManiaRuleset.CreatePerformanceCalculator(); - var performanceAttributes = - performanceCalculator.Calculate(scoreInfo, maniaDifficultyAttributes) - as ManiaPerformanceAttributes; - - return new ManiaCalculation() - { - Difficulty = GetDifficultyFromDifficultyAttributes(maniaDifficultyAttributes), - Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), - Accuracy = accuracy, - }; } private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId) diff --git a/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs b/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs index 7327400..8c34d6e 100644 --- a/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs +++ b/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs @@ -21,8 +21,8 @@ protected override CalculatorService< ); [Theory] - [InlineData(6.7171144000821119d, 291.34799376682508d, "diffcalc-test", new string[] { })] - [InlineData(8.9825709931204205d, 717.13844713272601d, "diffcalc-test", new string[] { "DT" })] + [InlineData(6.7171144000821119d, 291.6916492167043, "diffcalc-test", new string[] { })] + [InlineData(8.9825709931204205d, 718.5552449511403, "diffcalc-test", new string[] { "DT" })] public void Test( double expectedDifficultyTotal, double expectedPerformanceTotal, @@ -60,18 +60,37 @@ public void TestAllParameters() Misses = 5, Mehs = 4, Oks = 3, + SliderTails = 2, + SliderTicks = 1, }; - TestGetCalculationReturnsCorrectValues(12.418442356371395, 1415.202990027042, score); + TestGetCalculationReturnsCorrectValues(12.418442356371395, 1208.1384749524739, score); } [Fact] - public void TestClassicMod() + public void TestAllParametersClassicMod() { var score = new OsuScore { BeatmapId = "diffcalc-test", - Mods = [new Mod() { Acronym = "CL" }], + Mods = + [ + new Mod() { Acronym = "HD" }, + new Mod() { Acronym = "HR" }, + new Mod() + { + Acronym = "DT", + Settings = new Dictionary { { "speed_change", "2" } }, + }, + new Mod() { Acronym = "FL" }, + new Mod() { Acronym = "CL" }, + ], + Combo = 200, + Misses = 5, + Mehs = 4, + Oks = 3, + SliderTails = 2, + SliderTicks = 1, }; - TestGetCalculationReturnsCorrectValues(6.7171144000821119d, 289.16416504218972, score); + TestGetCalculationReturnsCorrectValues(12.418442356371395, 1405.0910286547635, score); } } diff --git a/Difficalcy.Osu/Models/OsuScore.cs b/Difficalcy.Osu/Models/OsuScore.cs index fd694d5..93749a3 100644 --- a/Difficalcy.Osu/Models/OsuScore.cs +++ b/Difficalcy.Osu/Models/OsuScore.cs @@ -18,6 +18,12 @@ public record OsuScore : Score, IValidatableObject [Range(0, int.MaxValue)] public int Oks { get; init; } = 0; + [Range(0, int.MaxValue)] + public int? SliderTails { get; init; } + + [Range(0, int.MaxValue)] + public int? SliderTicks { get; init; } // LargeTickHits + public IEnumerable Validate(ValidationContext validationContext) { if (Misses > 0 && Combo is null) diff --git a/Difficalcy.Osu/Services/OsuCalculatorService.cs b/Difficalcy.Osu/Services/OsuCalculatorService.cs index 01baf60..39fde0a 100644 --- a/Difficalcy.Osu/Services/OsuCalculatorService.cs +++ b/Difficalcy.Osu/Services/OsuCalculatorService.cs @@ -9,6 +9,7 @@ using osu.Game.Online.API; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Difficulty; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -97,6 +98,24 @@ object difficultyAttributes { var osuDifficultyAttributes = (OsuDifficultyAttributes)difficultyAttributes; + var scoreInfo = GetScoreInfo(score); + + var performanceCalculator = OsuRuleset.CreatePerformanceCalculator(); + var performanceAttributes = + performanceCalculator.Calculate(scoreInfo, osuDifficultyAttributes) + as OsuPerformanceAttributes; + + return new OsuCalculation() + { + Difficulty = GetDifficultyFromDifficultyAttributes(osuDifficultyAttributes), + Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), + Accuracy = scoreInfo.Accuracy, + Combo = scoreInfo.MaxCombo, + }; + } + + private ScoreInfo GetScoreInfo(OsuScore score) + { var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(OsuRuleset.RulesetInfo, mods); @@ -105,33 +124,48 @@ object difficultyAttributes score.Combo ?? beatmap.HitObjects.Count + beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); - var statistics = GetHitResults( - beatmap.HitObjects.Count, - score.Misses, - score.Mehs, - score.Oks - ); - var accuracy = CalculateAccuracy(statistics); - var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, OsuRuleset.RulesetInfo) + Dictionary statistics; + double accuracy; + + if (mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value)) { - Accuracy = accuracy, - MaxCombo = combo, - Statistics = statistics, - Mods = mods, - }; + statistics = GetClassicHitResults( + beatmap.HitObjects.Count, + score.Misses, + score.Mehs, + score.Oks + ); + accuracy = CalculateClassicAccuracy(statistics); + } + else + { + var maxSliderTails = beatmap.HitObjects.OfType().Count(); - var performanceCalculator = OsuRuleset.CreatePerformanceCalculator(); - var performanceAttributes = - performanceCalculator.Calculate(scoreInfo, osuDifficultyAttributes) - as OsuPerformanceAttributes; + var maxSliderTicks = beatmap + .HitObjects.OfType() + .Sum(s => s.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat)); - return new OsuCalculation() + statistics = GetLazerHitResults( + beatmap.HitObjects.Count, + maxSliderTails, + maxSliderTicks, + score.Misses, + score.Mehs, + score.Oks, + score.SliderTails, + score.SliderTicks + ); + + accuracy = CalculateLazerAccuracy(statistics, maxSliderTails, maxSliderTicks); + } + + return new ScoreInfo(beatmap.BeatmapInfo, OsuRuleset.RulesetInfo) { - Difficulty = GetDifficultyFromDifficultyAttributes(osuDifficultyAttributes), - Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), Accuracy = accuracy, - Combo = combo, + MaxCombo = combo, + Statistics = statistics, + Mods = mods, }; } @@ -150,7 +184,7 @@ private LazerMod ModToLazerMod(Mod mod) return apiMod.ToMod(OsuRuleset); } - private static Dictionary GetHitResults( + private static Dictionary GetClassicHitResults( int hitResultCount, int countMiss, int countMeh, @@ -168,18 +202,78 @@ int countOk }; } - private static double CalculateAccuracy(Dictionary statistics) + private static Dictionary GetLazerHitResults( + int hitResultCount, + int sliderTailCount, + int sliderTickCount, + int countMiss, + int countMeh, + int countOk, + int? countSliderTails, + int? countSliderTicks + ) + { + var countGreat = hitResultCount - countOk - countMeh - countMiss; + + return new Dictionary + { + { HitResult.Great, countGreat }, + { HitResult.Ok, countOk }, + { HitResult.Meh, countMeh }, + { HitResult.Miss, countMiss }, + { HitResult.SliderTailHit, countSliderTails ?? sliderTailCount }, + { HitResult.LargeTickHit, countSliderTicks ?? sliderTickCount }, + { + HitResult.LargeTickMiss, + sliderTickCount - (countSliderTicks ?? sliderTickCount) + }, + }; + } + + private static double CalculateClassicAccuracy(Dictionary statistics) + { + var countGreat = statistics[HitResult.Great]; + var countOk = statistics[HitResult.Ok]; + var countMeh = statistics[HitResult.Meh]; + var countMiss = statistics[HitResult.Miss]; + var countObjects = countGreat + countOk + countMeh + countMiss; + + if (countObjects == 0) + return 1; + + var max = countObjects * 6; + var total = (countGreat * 6) + (countOk * 2) + countMeh; + + return (double)total / max; + } + + private static double CalculateLazerAccuracy( + Dictionary statistics, + int sliderTailCount, + int sliderTickCount + ) { var countGreat = statistics[HitResult.Great]; var countOk = statistics[HitResult.Ok]; var countMeh = statistics[HitResult.Meh]; var countMiss = statistics[HitResult.Miss]; - var total = countGreat + countOk + countMeh + countMiss; + var countSliderTails = statistics[HitResult.SliderTailHit]; + var countSliderTicks = statistics[HitResult.LargeTickHit]; - if (total == 0) + var countObject = countGreat + countOk + countMeh + countMiss; + + if (countObject == 0) return 1; - return (double)((6 * countGreat) + (2 * countOk) + countMeh) / (6 * total); + var max = (countObject * 6) + (sliderTailCount * 3) + (sliderTickCount * 0.6); + var total = + (countGreat * 6) + + (countOk * 2) + + countMeh + + (countSliderTails * 3) + + (countSliderTicks * 0.6); + + return (double)total / max; } private static OsuDifficulty GetDifficultyFromDifficultyAttributes( diff --git a/Difficalcy.Taiko/Services/TaikoCalculatorService.cs b/Difficalcy.Taiko/Services/TaikoCalculatorService.cs index dd9959e..6d5f20f 100644 --- a/Difficalcy.Taiko/Services/TaikoCalculatorService.cs +++ b/Difficalcy.Taiko/Services/TaikoCalculatorService.cs @@ -7,7 +7,6 @@ using Difficalcy.Models; using Difficalcy.Services; using Difficalcy.Taiko.Models; -using osu.Game.Beatmaps.Legacy; using osu.Game.Online.API; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; @@ -94,6 +93,24 @@ object difficultyAttributes { var taikoDifficultyAttributes = (TaikoDifficultyAttributes)difficultyAttributes; + var scoreInfo = GetScoreInfo(score); + + var performanceCalculator = TaikoRuleset.CreatePerformanceCalculator(); + var performanceAttributes = + performanceCalculator.Calculate(scoreInfo, taikoDifficultyAttributes) + as TaikoPerformanceAttributes; + + return new TaikoCalculation() + { + Difficulty = GetDifficultyFromDifficultyAttributes(taikoDifficultyAttributes), + Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), + Accuracy = scoreInfo.Accuracy, + Combo = scoreInfo.MaxCombo, + }; + } + + private ScoreInfo GetScoreInfo(TaikoScore score) + { var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(TaikoRuleset.RulesetInfo, mods); @@ -103,26 +120,13 @@ object difficultyAttributes var statistics = GetHitResults(hitResultCount, score.Misses, score.Oks); var accuracy = CalculateAccuracy(statistics); - var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, TaikoRuleset.RulesetInfo) + return new ScoreInfo(beatmap.BeatmapInfo, TaikoRuleset.RulesetInfo) { Accuracy = accuracy, MaxCombo = combo, Statistics = statistics, Mods = mods, }; - - var performanceCalculator = TaikoRuleset.CreatePerformanceCalculator(); - var performanceAttributes = - performanceCalculator.Calculate(scoreInfo, taikoDifficultyAttributes) - as TaikoPerformanceAttributes; - - return new TaikoCalculation() - { - Difficulty = GetDifficultyFromDifficultyAttributes(taikoDifficultyAttributes), - Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes), - Accuracy = accuracy, - Combo = combo, - }; } private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId) diff --git a/compose.override.yaml b/compose.override.yaml index 6105afb..1d1b3ca 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -25,4 +25,4 @@ services: docs: ports: - - 8000:8000 + - 9000:8000 diff --git a/docs/docs/api-reference/difficalcy-osu.json b/docs/docs/api-reference/difficalcy-osu.json index 6f1daef..abdda3c 100644 --- a/docs/docs/api-reference/difficalcy-osu.json +++ b/docs/docs/api-reference/difficalcy-osu.json @@ -70,6 +70,26 @@ "format": "int32" } }, + { + "name": "SliderTails", + "in": "query", + "schema": { + "maximum": 2147483647, + "minimum": 0, + "type": "integer", + "format": "int32" + } + }, + { + "name": "SliderTicks", + "in": "query", + "schema": { + "maximum": 2147483647, + "minimum": 0, + "type": "integer", + "format": "int32" + } + }, { "name": "BeatmapId", "in": "query", @@ -296,6 +316,20 @@ "minimum": 0, "type": "integer", "format": "int32" + }, + "sliderTails": { + "maximum": 2147483647, + "minimum": 0, + "type": "integer", + "format": "int32", + "nullable": true + }, + "sliderTicks": { + "maximum": 2147483647, + "minimum": 0, + "type": "integer", + "format": "int32", + "nullable": true } }, "additionalProperties": false