diff --git a/Difficalcy.Catch.Tests/CatchCalculatorServiceTest.cs b/Difficalcy.Catch.Tests/CatchCalculatorServiceTest.cs index 4c43b3e..8bb21c3 100644 --- a/Difficalcy.Catch.Tests/CatchCalculatorServiceTest.cs +++ b/Difficalcy.Catch.Tests/CatchCalculatorServiceTest.cs @@ -1,5 +1,6 @@ using Difficalcy.Catch.Models; using Difficalcy.Catch.Services; +using Difficalcy.Models; using Difficalcy.Services; using Difficalcy.Tests; @@ -10,10 +11,10 @@ public class CatchCalculatorServiceTest : CalculatorServiceTest CalculatorService { get; } = new CatchCalculatorService(new InMemoryCache(), new TestBeatmapProvider(typeof(CatchCalculatorService).Assembly.GetName().Name)); [Theory] - [InlineData(4.0505463516206195d, 164.5770866821372d, "diffcalc-test", 0)] - [InlineData(5.1696411260785498d, 291.43480971713944d, "diffcalc-test", 64)] - public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods) - => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new CatchScore { BeatmapId = beatmapId, Mods = mods }); + [InlineData(4.0505463516206195d, 164.5770866821372d, "diffcalc-test", new string[] { })] + [InlineData(5.1696411260785498d, 291.43480971713944d, "diffcalc-test", new string[] { "DT" })] + public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, string[] mods) + => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new CatchScore { BeatmapId = beatmapId, Mods = mods.Select(m => new Mod { Acronym = m }).ToArray() }); [Fact] public void TestAllParameters() @@ -21,12 +22,22 @@ public void TestAllParameters() var score = new CatchScore { BeatmapId = "diffcalc-test", - Mods = 80, // HR, DT + Mods = [ + new Mod() { Acronym = "HR" }, + new Mod() + { + Acronym = "DT", + Settings = new Dictionary + { + { "speed_change", "2" } + } + } + ], Combo = 100, Misses = 5, LargeDroplets = 18, SmallDroplets = 200, }; - TestGetCalculationReturnsCorrectValues(5.739025024925009d, 241.19384779497875d, score); + TestGetCalculationReturnsCorrectValues(6.9017468199992278, 375.74458599075302, score); } } diff --git a/Difficalcy.Catch/Services/CatchCalculatorService.cs b/Difficalcy.Catch/Services/CatchCalculatorService.cs index 3636e70..0bf8902 100644 --- a/Difficalcy.Catch/Services/CatchCalculatorService.cs +++ b/Difficalcy.Catch/Services/CatchCalculatorService.cs @@ -9,11 +9,13 @@ 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; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using LazerMod = osu.Game.Rulesets.Mods.Mod; namespace Difficalcy.Catch.Services { @@ -43,13 +45,13 @@ protected override async Task EnsureBeatmap(string beatmapId) await beatmapProvider.EnsureBeatmap(beatmapId); } - protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods) + protected override (object, string) CalculateDifficultyAttributes(string beatmapId, Mod[] mods) { var workingBeatmap = GetWorkingBeatmap(beatmapId); - var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray(); + var lazerMods = mods.Select(ModToLazerMod).ToArray(); var difficultyCalculator = CatchRuleset.CreateDifficultyCalculator(workingBeatmap); - var difficultyAttributes = difficultyCalculator.Calculate(mods) as CatchDifficultyAttributes; + var difficultyAttributes = difficultyCalculator.Calculate(lazerMods) as CatchDifficultyAttributes; // Serialising anonymous object with same names because some properties can't be serialised, and the built-in JsonProperty fields aren't on all required fields return (difficultyAttributes, JsonSerializer.Serialize(new @@ -70,7 +72,7 @@ protected override CatchCalculation CalculatePerformance(CatchScore score, objec var catchDifficultyAttributes = (CatchDifficultyAttributes)difficultyAttributes; var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); - var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray(); + var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(CatchRuleset.RulesetInfo, mods); var combo = score.Combo ?? beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)); @@ -103,6 +105,15 @@ private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId) return new CalculatorWorkingBeatmap(CatchRuleset, beatmapStream); } + private LazerMod ModToLazerMod(Mod mod) + { + var apiMod = new APIMod { Acronym = mod.Acronym }; + foreach (var setting in mod.Settings) + apiMod.Settings.Add(setting.Key, setting.Value); + + return apiMod.ToMod(CatchRuleset); + } + private static Dictionary GetHitResults(IBeatmap beatmap, int countMiss, int? countDroplet, int? countTinyDroplet) { var maxTinyDroplets = beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.OfType().Count()); diff --git a/Difficalcy.Mania.Tests/ManiaCalculatorServiceTest.cs b/Difficalcy.Mania.Tests/ManiaCalculatorServiceTest.cs index 2b3de7f..bc93bff 100644 --- a/Difficalcy.Mania.Tests/ManiaCalculatorServiceTest.cs +++ b/Difficalcy.Mania.Tests/ManiaCalculatorServiceTest.cs @@ -1,5 +1,6 @@ using Difficalcy.Mania.Models; using Difficalcy.Mania.Services; +using Difficalcy.Models; using Difficalcy.Services; using Difficalcy.Tests; @@ -10,10 +11,10 @@ public class ManiaCalculatorServiceTest : CalculatorServiceTest CalculatorService { get; } = new ManiaCalculatorService(new InMemoryCache(), new TestBeatmapProvider(typeof(ManiaCalculatorService).Assembly.GetName().Name)); [Theory] - [InlineData(2.3493769750220914d, 45.76140071089439d, "diffcalc-test", 0)] - [InlineData(2.797245912537965d, 68.79984443279172d, "diffcalc-test", 64)] - public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods) - => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new ManiaScore { BeatmapId = beatmapId, Mods = mods }); + [InlineData(2.3493769750220914d, 45.76140071089439d, "diffcalc-test", new string[] { })] + [InlineData(2.797245912537965d, 68.79984443279172d, "diffcalc-test", new string[] { "DT" })] + public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, string[] mods) + => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new ManiaScore { BeatmapId = beatmapId, Mods = mods.Select(m => new Mod { Acronym = m }).ToArray() }); [Fact] public void TestAllParameters() @@ -21,13 +22,22 @@ public void TestAllParameters() var score = new ManiaScore { BeatmapId = "diffcalc-test", - Mods = 64, // DT + Mods = [ + new Mod() + { + Acronym = "DT", + Settings = new Dictionary + { + { "speed_change", "2" } + } + } + ], Misses = 5, Mehs = 4, Oks = 3, Goods = 2, Greats = 1, }; - TestGetCalculationReturnsCorrectValues(2.797245912537965d, 43.17076331130473d, score); + TestGetCalculationReturnsCorrectValues(3.3252153148972425, 64.408516282383957, score); } } diff --git a/Difficalcy.Mania/Services/ManiaCalculatorService.cs b/Difficalcy.Mania/Services/ManiaCalculatorService.cs index e7554e6..c3b15b6 100644 --- a/Difficalcy.Mania/Services/ManiaCalculatorService.cs +++ b/Difficalcy.Mania/Services/ManiaCalculatorService.cs @@ -8,11 +8,13 @@ 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; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using LazerMod = osu.Game.Rulesets.Mods.Mod; namespace Difficalcy.Mania.Services { @@ -43,13 +45,13 @@ protected override async Task EnsureBeatmap(string beatmapId) await _beatmapProvider.EnsureBeatmap(beatmapId); } - protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods) + protected override (object, string) CalculateDifficultyAttributes(string beatmapId, Mod[] mods) { var workingBeatmap = GetWorkingBeatmap(beatmapId); - var mods = ManiaRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray(); + var lazerMods = mods.Select(ModToLazerMod).ToArray(); var difficultyCalculator = ManiaRuleset.CreateDifficultyCalculator(workingBeatmap); - var difficultyAttributes = difficultyCalculator.Calculate(mods) as ManiaDifficultyAttributes; + var difficultyAttributes = difficultyCalculator.Calculate(lazerMods) as ManiaDifficultyAttributes; // Serialising anonymous object with same names because some properties can't be serialised, and the built-in JsonProperty fields aren't on all required fields return (difficultyAttributes, JsonSerializer.Serialize(new @@ -69,7 +71,7 @@ protected override ManiaCalculation CalculatePerformance(ManiaScore score, objec { var maniaDifficultyAttributes = (ManiaDifficultyAttributes)difficultyAttributes; var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); - var mods = ManiaRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray(); + var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(ManiaRuleset.RulesetInfo, mods); var hitObjectCount = beatmap.HitObjects.Count; @@ -102,6 +104,15 @@ private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId) return new CalculatorWorkingBeatmap(ManiaRuleset, beatmapStream); } + private LazerMod ModToLazerMod(Mod mod) + { + var apiMod = new APIMod { Acronym = mod.Acronym }; + foreach (var setting in mod.Settings) + apiMod.Settings.Add(setting.Key, setting.Value); + + return apiMod.ToMod(ManiaRuleset); + } + private static Dictionary GetHitResults(int hitResultCount, int countMiss, int countMeh, int countOk, int countGood, int countGreat) { var countPerfect = hitResultCount - (countMiss + countMeh + countOk + countGood + countGreat); diff --git a/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs b/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs index dd674a8..32e0a3c 100644 --- a/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs +++ b/Difficalcy.Osu.Tests/OsuCalculatorServiceTest.cs @@ -1,3 +1,4 @@ +using Difficalcy.Models; using Difficalcy.Osu.Models; using Difficalcy.Osu.Services; using Difficalcy.Services; @@ -10,10 +11,10 @@ public class OsuCalculatorServiceTest : CalculatorServiceTest CalculatorService { get; } = new OsuCalculatorService(new InMemoryCache(), new TestBeatmapProvider(typeof(OsuCalculatorService).Assembly.GetName().Name)); [Theory] - [InlineData(6.7171144000821119d, 291.34799376682508d, "diffcalc-test", 0)] - [InlineData(8.9825709931204205d, 717.13844713272601d, "diffcalc-test", 64)] - public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods) - => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new OsuScore { BeatmapId = beatmapId, Mods = mods }); + [InlineData(6.7171144000821119d, 291.34799376682508d, "diffcalc-test", new string[] { })] + [InlineData(8.9825709931204205d, 717.13844713272601d, "diffcalc-test", new string[] { "DT" })] + public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, string[] mods) + => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new OsuScore { BeatmapId = beatmapId, Mods = mods.Select(m => new Mod { Acronym = m }).ToArray() }); [Fact] public void TestAllParameters() @@ -21,12 +22,37 @@ public void TestAllParameters() var score = new OsuScore { BeatmapId = "diffcalc-test", - Mods = 1112, // HD, HR, DT, FL + Mods = [ + new Mod() { Acronym = "HD" }, + new Mod() { Acronym = "HR" }, + new Mod() + { + Acronym = "DT", + Settings = new Dictionary + { + { "speed_change", "2" } + } + }, + new Mod() { Acronym = "FL" } + ], Combo = 200, Misses = 5, Mehs = 4, Oks = 3, }; - TestGetCalculationReturnsCorrectValues(10.095171949076231d, 685.8314990408466d, score); + TestGetCalculationReturnsCorrectValues(12.418442356371395, 1415.202990027042, score); + } + + [Fact] + public void TestClassicMod() + { + var score = new OsuScore + { + BeatmapId = "diffcalc-test", + Mods = [ + new Mod() { Acronym = "CL" } + ], + }; + TestGetCalculationReturnsCorrectValues(6.7171144000821119d, 289.16416504218972, score); } } diff --git a/Difficalcy.Osu/Services/OsuCalculatorService.cs b/Difficalcy.Osu/Services/OsuCalculatorService.cs index 9f7729a..38def1f 100644 --- a/Difficalcy.Osu/Services/OsuCalculatorService.cs +++ b/Difficalcy.Osu/Services/OsuCalculatorService.cs @@ -6,12 +6,13 @@ using Difficalcy.Models; using Difficalcy.Osu.Models; using Difficalcy.Services; -using osu.Game.Beatmaps.Legacy; +using osu.Game.Online.API; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Difficulty; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using LazerMod = osu.Game.Rulesets.Mods.Mod; namespace Difficalcy.Osu.Services { @@ -41,13 +42,13 @@ protected override async Task EnsureBeatmap(string beatmapId) await beatmapProvider.EnsureBeatmap(beatmapId); } - protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods) + protected override (object, string) CalculateDifficultyAttributes(string beatmapId, Mod[] mods) { var workingBeatmap = GetWorkingBeatmap(beatmapId); - var mods = OsuRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray(); + var lazerMods = mods.Select(ModToLazerMod).ToArray(); var difficultyCalculator = OsuRuleset.CreateDifficultyCalculator(workingBeatmap); - var difficultyAttributes = difficultyCalculator.Calculate(mods) as OsuDifficultyAttributes; + var difficultyAttributes = difficultyCalculator.Calculate(lazerMods) as OsuDifficultyAttributes; // Serialising anonymous object with same names because some properties can't be serialised, and the built-in JsonProperty fields aren't on all required fields return (difficultyAttributes, JsonSerializer.Serialize(new @@ -80,7 +81,7 @@ protected override OsuCalculation CalculatePerformance(OsuScore score, object di var osuDifficultyAttributes = (OsuDifficultyAttributes)difficultyAttributes; var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); - var mods = OsuRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray(); + var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(OsuRuleset.RulesetInfo, mods); var combo = score.Combo ?? beatmap.HitObjects.Count + beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); @@ -113,6 +114,15 @@ private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId) return new CalculatorWorkingBeatmap(OsuRuleset, beatmapStream); } + private LazerMod ModToLazerMod(Mod mod) + { + var apiMod = new APIMod { Acronym = mod.Acronym }; + foreach (var setting in mod.Settings) + apiMod.Settings.Add(setting.Key, setting.Value); + + return apiMod.ToMod(OsuRuleset); + } + private static Dictionary GetHitResults(int hitResultCount, int countMiss, int countMeh, int countOk) { var countGreat = hitResultCount - countOk - countMeh - countMiss; diff --git a/Difficalcy.Taiko.Tests/TaikoCalculatorServiceTest.cs b/Difficalcy.Taiko.Tests/TaikoCalculatorServiceTest.cs index 7251480..7d60f03 100644 --- a/Difficalcy.Taiko.Tests/TaikoCalculatorServiceTest.cs +++ b/Difficalcy.Taiko.Tests/TaikoCalculatorServiceTest.cs @@ -2,6 +2,7 @@ using Difficalcy.Taiko.Services; using Difficalcy.Services; using Difficalcy.Tests; +using Difficalcy.Models; namespace Difficalcy.Taiko.Tests; @@ -10,10 +11,10 @@ public class TaikoCalculatorServiceTest : CalculatorServiceTest CalculatorService { get; } = new TaikoCalculatorService(new InMemoryCache(), new TestBeatmapProvider(typeof(TaikoCalculatorService).Assembly.GetName().Name)); [Theory] - [InlineData(3.092021259435121d, 137.80325540434842d, "diffcalc-test", 0)] - [InlineData(4.0789820318081444d, 248.8310568362074d, "diffcalc-test", 64)] - public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods) - => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new TaikoScore { BeatmapId = beatmapId, Mods = mods }); + [InlineData(3.092021259435121d, 137.80325540434842d, "diffcalc-test", new string[] { })] + [InlineData(4.0789820318081444d, 248.8310568362074d, "diffcalc-test", new string[] { "DT" })] + public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, string[] mods) + => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new TaikoScore { BeatmapId = beatmapId, Mods = mods.Select(m => new Mod { Acronym = m }).ToArray() }); [Fact] public void TestAllParameters() @@ -21,11 +22,21 @@ public void TestAllParameters() var score = new TaikoScore { BeatmapId = "diffcalc-test", - Mods = 80, // HR, DT + Mods = [ + new Mod() { Acronym = "HR" }, + new Mod() + { + Acronym = "DT", + Settings = new Dictionary + { + { "speed_change", "2" } + } + } + ], Combo = 150, Misses = 5, Oks = 3, }; - TestGetCalculationReturnsCorrectValues(4.0789820318081444d, 240.24516772998618d, score); + TestGetCalculationReturnsCorrectValues(4.922364692298034, 359.95282202016443, score); } } diff --git a/Difficalcy.Taiko/Services/TaikoCalculatorService.cs b/Difficalcy.Taiko/Services/TaikoCalculatorService.cs index 28a6354..453c487 100644 --- a/Difficalcy.Taiko/Services/TaikoCalculatorService.cs +++ b/Difficalcy.Taiko/Services/TaikoCalculatorService.cs @@ -8,11 +8,13 @@ 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; using osu.Game.Rulesets.Taiko.Difficulty; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; +using LazerMod = osu.Game.Rulesets.Mods.Mod; namespace Difficalcy.Taiko.Services { @@ -43,13 +45,13 @@ protected override async Task EnsureBeatmap(string beatmapId) await _beatmapProvider.EnsureBeatmap(beatmapId); } - protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods) + protected override (object, string) CalculateDifficultyAttributes(string beatmapId, Mod[] mods) { var workingBeatmap = GetWorkingBeatmap(beatmapId); - var mods = TaikoRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray(); + var lazerMods = mods.Select(ModToLazerMod).ToArray(); var difficultyCalculator = TaikoRuleset.CreateDifficultyCalculator(workingBeatmap); - var difficultyAttributes = difficultyCalculator.Calculate(mods) as TaikoDifficultyAttributes; + var difficultyAttributes = difficultyCalculator.Calculate(lazerMods) as TaikoDifficultyAttributes; // Serialising anonymous object with same names because some properties can't be serialised, and the built-in JsonProperty fields aren't on all required fields return (difficultyAttributes, JsonSerializer.Serialize(new @@ -75,7 +77,7 @@ protected override TaikoCalculation CalculatePerformance(TaikoScore score, objec var taikoDifficultyAttributes = (TaikoDifficultyAttributes)difficultyAttributes; var workingBeatmap = GetWorkingBeatmap(score.BeatmapId); - var mods = TaikoRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray(); + var mods = score.Mods.Select(ModToLazerMod).ToArray(); var beatmap = workingBeatmap.GetPlayableBeatmap(TaikoRuleset.RulesetInfo, mods); var hitResultCount = beatmap.HitObjects.OfType().Count(); @@ -109,6 +111,15 @@ private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId) return new CalculatorWorkingBeatmap(TaikoRuleset, beatmapStream); } + private LazerMod ModToLazerMod(Mod mod) + { + var apiMod = new APIMod { Acronym = mod.Acronym }; + foreach (var setting in mod.Settings) + apiMod.Settings.Add(setting.Key, setting.Value); + + return apiMod.ToMod(TaikoRuleset); + } + private static Dictionary GetHitResults(int hitResultCount, int countMiss, int countOk) { var countGreat = hitResultCount - countOk - countMiss; diff --git a/Difficalcy.Tests/DummyCalculatorServiceTest.cs b/Difficalcy.Tests/DummyCalculatorServiceTest.cs index 86c002c..21806bb 100644 --- a/Difficalcy.Tests/DummyCalculatorServiceTest.cs +++ b/Difficalcy.Tests/DummyCalculatorServiceTest.cs @@ -8,10 +8,10 @@ public class DummyCalculatorServiceTest : CalculatorServiceTest CalculatorService { get; } = new DummyCalculatorService(new InMemoryCache()); [Theory] - [InlineData(15, 1500, "test 1", 150)] - [InlineData(10, 1000, "test 2", 100)] - public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods) - => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new DummyScore { BeatmapId = beatmapId, Mods = mods, Points = 100 }); + [InlineData(15, 1500, "test 1", new string[] { "150" })] + [InlineData(10, 1000, "test 2", new string[] { "25", "75" })] + public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, string[] mods) + => TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new DummyScore { BeatmapId = beatmapId, Mods = mods.Select(m => new Mod { Acronym = m }).ToArray(), Points = 100 }); [Fact] public async Task TestGetCalculationBatchReturnsCorrectValuesInOrder() @@ -19,18 +19,18 @@ public async Task TestGetCalculationBatchReturnsCorrectValuesInOrder() // values are intentionally in a random order to ensure unique beatmap grouping doesnt break return ordering var scores = new[] { - new DummyScore { BeatmapId = "test 1", Mods = 200, Points = 200 }, // 3 - new DummyScore { BeatmapId = "test 2", Mods = 300, Points = 100 }, // 4 - new DummyScore { BeatmapId = "test 2", Mods = 300, Points = 200 }, // 5 - new DummyScore { BeatmapId = "test 3", Mods = 500, Points = 200 }, // 9 - new DummyScore { BeatmapId = "test 2", Mods = 400, Points = 200 }, // 7 - new DummyScore { BeatmapId = "test 1", Mods = 200, Points = 100 }, // 2 - new DummyScore { BeatmapId = "test 3", Mods = 600, Points = 100 }, // 10 - new DummyScore { BeatmapId = "test 2", Mods = 400, Points = 100 }, // 6 - new DummyScore { BeatmapId = "test 3", Mods = 500, Points = 100 }, // 8 - new DummyScore { BeatmapId = "test 1", Mods = 100, Points = 200 }, // 1 - new DummyScore { BeatmapId = "test 3", Mods = 600, Points = 200 }, // 11 - new DummyScore { BeatmapId = "test 1", Mods = 100, Points = 100 }, // 0 + new DummyScore { BeatmapId = "test 1", Mods = [new Mod() { Acronym = "200" }], Points = 200 }, // 3 + new DummyScore { BeatmapId = "test 2", Mods = [new Mod() { Acronym = "300" }], Points = 100 }, // 4 + new DummyScore { BeatmapId = "test 2", Mods = [new Mod() { Acronym = "300" }], Points = 200 }, // 5 + new DummyScore { BeatmapId = "test 3", Mods = [new Mod() { Acronym = "500" }], Points = 200 }, // 9 + new DummyScore { BeatmapId = "test 2", Mods = [new Mod() { Acronym = "400" }], Points = 200 }, // 7 + new DummyScore { BeatmapId = "test 1", Mods = [new Mod() { Acronym = "200" }], Points = 100 }, // 2 + new DummyScore { BeatmapId = "test 3", Mods = [new Mod() { Acronym = "600" }], Points = 100 }, // 10 + new DummyScore { BeatmapId = "test 2", Mods = [new Mod() { Acronym = "400" }], Points = 100 }, // 6 + new DummyScore { BeatmapId = "test 3", Mods = [new Mod() { Acronym = "500" }], Points = 100 }, // 8 + new DummyScore { BeatmapId = "test 1", Mods = [new Mod() { Acronym = "100" }], Points = 200 }, // 1 + new DummyScore { BeatmapId = "test 3", Mods = [new Mod() { Acronym = "600" }], Points = 200 }, // 11 + new DummyScore { BeatmapId = "test 1", Mods = [new Mod() { Acronym = "100" }], Points = 100 }, // 0 }; var calculations = (await CalculatorService.GetCalculationBatch(scores)).ToArray(); @@ -76,7 +76,7 @@ public async Task TestGetCalculationBatchReturnsCorrectValuesInOrder() } /// -/// A dummy calculator service implementation that calculates difficulty as (beatmap id + mods) / 10 and performance as difficulty * 100 +/// A dummy calculator service implementation that calculates difficulty as mods (casted from string to double) / 10 and performance as difficulty * points /// public class DummyCalculatorService(ICache cache) : CalculatorService(cache) { @@ -90,9 +90,9 @@ public class DummyCalculatorService(ICache cache) : CalculatorService double.Parse(m.Acronym)) / 10; return (difficulty, difficulty.ToString()); } diff --git a/Difficalcy/Models/Score.cs b/Difficalcy/Models/Score.cs index ef1e203..6c2cb3b 100644 --- a/Difficalcy/Models/Score.cs +++ b/Difficalcy/Models/Score.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; namespace Difficalcy.Models { @@ -7,7 +9,24 @@ public abstract record Score [Required] public string BeatmapId { get; init; } - [Range(0, int.MaxValue)] - public int Mods { get; init; } = 0; + public Mod[] Mods { get; init; } = []; + } + + public record Mod + { + [Required] + public string Acronym { get; init; } + + public Dictionary Settings { get; init; } = []; + + public override string ToString() + { + if (Settings.Count == 0) + return Acronym; + + var settingsString = string.Join(",", Settings.OrderBy(setting => setting.Key).Select(setting => $"{setting.Key}={setting.Value}")); + + return $"{Acronym}({settingsString})"; + } } } diff --git a/Difficalcy/Services/CalculatorService.cs b/Difficalcy/Services/CalculatorService.cs index 32c4bcc..33d5221 100644 --- a/Difficalcy/Services/CalculatorService.cs +++ b/Difficalcy/Services/CalculatorService.cs @@ -30,7 +30,7 @@ public abstract class CalculatorService /// Runs the difficulty calculator and returns the difficulty attributes as both an object and JSON serialised string. /// - protected abstract (object, string) CalculateDifficultyAttributes(string beatmapId, int mods); + protected abstract (object, string) CalculateDifficultyAttributes(string beatmapId, Mod[] mods); /// /// Returns the deserialised object for a given JSON serialised difficulty attributes object. @@ -54,29 +54,29 @@ public async Task GetCalculation(TScore score) public async Task> GetCalculationBatch(TScore[] scores) { var scoresWithIndex = scores.Select((score, index) => (score, index)); - var uniqueBeatmapGroups = scoresWithIndex.GroupBy(scoreWithIndex => (scoreWithIndex.score.BeatmapId, scoreWithIndex.score.Mods)); + var uniqueBeatmapGroups = scoresWithIndex.GroupBy(scoreWithIndex => (scoreWithIndex.score.BeatmapId, GetModString(scoreWithIndex.score.Mods))); var calculationGroups = await Task.WhenAll(uniqueBeatmapGroups.Select(async group => { var scores = group.Select(scoreWithIndex => scoreWithIndex.score); - return group.Select(scoreWithIndex => scoreWithIndex.index).Zip(await GetUniqueBeatmapCalculationBatch(group.Key.BeatmapId, group.Key.Mods, scores)); + return group.Select(scoreWithIndex => scoreWithIndex.index).Zip(await GetUniqueBeatmapCalculationBatch(group.Key.BeatmapId, scores.First().Mods, scores)); })); return calculationGroups.SelectMany(group => group).OrderBy(group => group.First).Select(group => group.Second); } - private async Task> GetUniqueBeatmapCalculationBatch(string beatmapId, int mods, IEnumerable scores) + private async Task> GetUniqueBeatmapCalculationBatch(string beatmapId, Mod[] mods, IEnumerable scores) { var difficultyAttributes = await GetDifficultyAttributes(beatmapId, mods); return scores.AsParallel().AsOrdered().Select(score => CalculatePerformance(score, difficultyAttributes)); } - private async Task GetDifficultyAttributes(string beatmapId, int mods) + private async Task GetDifficultyAttributes(string beatmapId, Mod[] mods) { await EnsureBeatmap(beatmapId); var db = cache.GetDatabase(); - var redisKey = $"difficalcy:{CalculatorDiscriminator}:{beatmapId}:{mods}"; + var redisKey = GetRedisKey(beatmapId, mods); var difficultyAttributesJson = await db.GetAsync(redisKey); object difficultyAttributes; @@ -92,5 +92,9 @@ private async Task GetDifficultyAttributes(string beatmapId, int mods) return difficultyAttributes; } + + private string GetRedisKey(string beatmapId, Mod[] mods) => $"difficalcy:{CalculatorDiscriminator}:{beatmapId}:{GetModString(mods)}"; + + private static string GetModString(Mod[] mods) => string.Join(",", mods.OrderBy(mod => mod.Acronym).Select(mod => mod.ToString())); } } diff --git a/docs/docs/api-reference/difficalcy-catch.json b/docs/docs/api-reference/difficalcy-catch.json index f1fa889..a2b5612 100644 --- a/docs/docs/api-reference/difficalcy-catch.json +++ b/docs/docs/api-reference/difficalcy-catch.json @@ -82,10 +82,10 @@ "name": "Mods", "in": "query", "schema": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + } } } ], @@ -217,10 +217,11 @@ "type": "string" }, "mods": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + }, + "nullable": true }, "combo": { "maximum": 2147483647, @@ -251,6 +252,27 @@ } }, "additionalProperties": false + }, + "Mod": { + "required": [ + "acronym" + ], + "type": "object", + "properties": { + "acronym": { + "minLength": 1, + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + } + }, + "additionalProperties": false } } } diff --git a/docs/docs/api-reference/difficalcy-mania.json b/docs/docs/api-reference/difficalcy-mania.json index 0fd7ae9..2fc3853 100644 --- a/docs/docs/api-reference/difficalcy-mania.json +++ b/docs/docs/api-reference/difficalcy-mania.json @@ -92,10 +92,10 @@ "name": "Mods", "in": "query", "schema": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + } } } ], @@ -227,10 +227,11 @@ "type": "string" }, "mods": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + }, + "nullable": true }, "misses": { "maximum": 2147483647, @@ -264,6 +265,27 @@ } }, "additionalProperties": false + }, + "Mod": { + "required": [ + "acronym" + ], + "type": "object", + "properties": { + "acronym": { + "minLength": 1, + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + } + }, + "additionalProperties": false } } } diff --git a/docs/docs/api-reference/difficalcy-osu.json b/docs/docs/api-reference/difficalcy-osu.json index b22d727..6f1daef 100644 --- a/docs/docs/api-reference/difficalcy-osu.json +++ b/docs/docs/api-reference/difficalcy-osu.json @@ -82,10 +82,10 @@ "name": "Mods", "in": "query", "schema": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + } } } ], @@ -166,6 +166,27 @@ }, "additionalProperties": false }, + "Mod": { + "required": [ + "acronym" + ], + "type": "object", + "properties": { + "acronym": { + "minLength": 1, + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + } + }, + "additionalProperties": false + }, "OsuCalculation": { "type": "object", "properties": { @@ -245,10 +266,11 @@ "type": "string" }, "mods": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + }, + "nullable": true }, "combo": { "maximum": 2147483647, diff --git a/docs/docs/api-reference/difficalcy-taiko.json b/docs/docs/api-reference/difficalcy-taiko.json index 051a6c0..e4033f8 100644 --- a/docs/docs/api-reference/difficalcy-taiko.json +++ b/docs/docs/api-reference/difficalcy-taiko.json @@ -72,10 +72,10 @@ "name": "Mods", "in": "query", "schema": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + } } } ], @@ -156,6 +156,27 @@ }, "additionalProperties": false }, + "Mod": { + "required": [ + "acronym" + ], + "type": "object", + "properties": { + "acronym": { + "minLength": 1, + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + } + }, + "additionalProperties": false + }, "TaikoCalculation": { "type": "object", "properties": { @@ -227,10 +248,11 @@ "type": "string" }, "mods": { - "maximum": 2147483647, - "minimum": 0, - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "$ref": "#/components/schemas/Mod" + }, + "nullable": true }, "combo": { "maximum": 2147483647,