diff --git a/osu.Game.Benchmarks/BenchmarkScoreMultiplierCalculator.cs b/osu.Game.Benchmarks/BenchmarkScoreMultiplierCalculator.cs index 22e8b41038..e2d4c79c71 100644 --- a/osu.Game.Benchmarks/BenchmarkScoreMultiplierCalculator.cs +++ b/osu.Game.Benchmarks/BenchmarkScoreMultiplierCalculator.cs @@ -42,29 +42,7 @@ namespace osu.Game.Benchmarks public override void SetUp() { base.SetUp(); - calculator = new OsuRuleset().CreateScoreMultiplierCalculator(); - } - - [Benchmark] - public double ViaModScoreMultiplier() => viaModScoreMultiplier(Times, Mods); - - [Test] - public void ViaModScoreMultiplier([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods) - => viaModScoreMultiplier(times, mods); - - private double viaModScoreMultiplier(int times, ModTestCase mods) - { - double scoreMultiplier = 1; - - for (int i = 0; i < times; ++i) - { - scoreMultiplier = 1; - - foreach (var mod in mods.Mods) - scoreMultiplier *= mod.ScoreMultiplier; - } - - return scoreMultiplier; + calculator = new OsuRuleset().CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); } [Benchmark] diff --git a/osu.Game.Rulesets.Catch.Tests/CatchScoreMultiplierTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchScoreMultiplierTest.cs index a0f8a948c5..7642af08eb 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchScoreMultiplierTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchScoreMultiplierTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Rulesets; namespace osu.Game.Rulesets.Catch.Tests @@ -14,28 +15,141 @@ namespace osu.Game.Rulesets.Catch.Tests { } - [Test] - public void TestFlashlightOnNonDefaultSettings() - => TestModCombination([new CatchModFlashlight { ComboBasedSize = { Value = false } }]); + private static readonly object[][] test_cases = + [ + #region Difficulty Reduction - [Test] - public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new CatchModHalfTime { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new CatchModEasy() }, 0.5], + [new Mod[] { new CatchModNoFail() }, 0.5], - [Test] - public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new CatchModDaycore { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new CatchModDoubleTime { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new CatchModNightcore { SpeedChange = { Value = speedChange } }]); + #endregion - [Test] - public void TestMultiplicativeCombination() - => TestModCombination([new CatchModHidden(), new CatchModHardRock()]); + #region Difficulty Increase + + [new Mod[] { new CatchModHardRock() }, 1.12], + [new Mod[] { new CatchModSuddenDeath() }, 1], + [new Mod[] { new CatchModPerfect() }, 1], + + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18], + [new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20], + + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18], + [new Mod[] { new CatchModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20], + + [new Mod[] { new CatchModHidden() }, 1.06], + + [new Mod[] { new CatchModFlashlight() }, 1.12], + [new Mod[] { new CatchModFlashlight { ComboBasedSize = { Value = false } } }, 1], + + [new Mod[] { new ModAccuracyChallenge() }, 1], + + #endregion + + #region Conversion + + [new Mod[] { new CatchModDifficultyAdjust() }, 0.5], + [new Mod[] { new CatchModClassic() }, 0.96], + [new Mod[] { new CatchModMirror() }, 1], + + #endregion + + #region Automation + + [new Mod[] { new CatchModAutoplay() }, 1], + [new Mod[] { new CatchModCinema() }, 1], + [new Mod[] { new CatchModRelax() }, 0.1], + + #endregion + + #region Fun + + [new Mod[] { new ModWindUp() }, 0.5], + [new Mod[] { new ModWindDown() }, 0.5], + [new Mod[] { new CatchModFloatingFruits() }, 1], + [new Mod[] { new CatchModMuted() }, 1], + [new Mod[] { new CatchModNoScope() }, 1], + [new Mod[] { new CatchModMovingFast() }, 1], + [new Mod[] { new CatchModSynesthesia() }, 0.8], + + #endregion + + #region System + + [new Mod[] { new ModScoreV2() }, 1], + + #endregion + + #region Combinations + + [new Mod[] { new CatchModHidden(), new CatchModHardRock() }, 1.06 * 1.12] + + #endregion + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestMultipliers(Mod[] mods, double expectedMultiplier) + => TestModCombination(mods, expectedMultiplier); } } diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 61831fdb1e..ea20e0c751 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Catch } } - public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new CatchScoreMultiplierCalculator(); + public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new CatchScoreMultiplierCalculator(context); public override string Description => "osu!catch"; diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreMultiplierCalculator.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreMultiplierCalculator.cs index 55ba80a4a0..0c5b776b57 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreMultiplierCalculator.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreMultiplierCalculator.cs @@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Catch.Scoring { public class CatchScoreMultiplierCalculator : ScoreMultiplierCalculator { - static CatchScoreMultiplierCalculator() + public CatchScoreMultiplierCalculator(ScoreMultiplierContext context) + : base(context) { #region Difficulty Reduction diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaScoreMultiplierTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaScoreMultiplierTest.cs index 730210270e..e07d80d5fa 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaScoreMultiplierTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaScoreMultiplierTest.cs @@ -1,8 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Tests.Rulesets; namespace osu.Game.Rulesets.Mania.Tests @@ -14,24 +19,205 @@ namespace osu.Game.Rulesets.Mania.Tests { } - [Test] - public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new ManiaModHalfTime { SpeedChange = { Value = speedChange } }]); + private static readonly object[][] test_cases = + [ + #region Difficulty Reduction - [Test] - public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new ManiaModDaycore { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new ManiaModEasy() }, 0.5], + [new Mod[] { new ManiaModNoFail() }, 0.5], - [Test] - public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new ManiaModDoubleTime { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new ManiaModNightcore { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestMultiplicativeCombination() - => TestModCombination([new ManiaModEasy(), new ManiaModKey4()]); + [new Mod[] { new ManiaModNoRelease() }, 0.9], + + #endregion + + #region Difficulty Increase + + [new Mod[] { new ManiaModHardRock() }, 1], + [new Mod[] { new ManiaModSuddenDeath() }, 1], + [new Mod[] { new ManiaModPerfect() }, 1], + + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1], + [new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1], + + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.01 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.05 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.10 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.15 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.20 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.25 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.30 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.35 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.40 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.45 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.50 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.55 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.60 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.65 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.70 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.75 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.80 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.85 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.90 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.95 } } }, 1], + [new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 2.00 } } }, 1], + + [new Mod[] { new ManiaModFadeIn() }, 1], + [new Mod[] { new ManiaModHidden() }, 1], + [new Mod[] { new ManiaModCover() }, 1], + + [new Mod[] { new ManiaModFlashlight() }, 1], + [new Mod[] { new ModAccuracyChallenge() }, 1], + + #endregion + + #region Conversion + + [new Mod[] { new ManiaModRandom() }, 1], + [new Mod[] { new ManiaModDualStages() }, 1], + [new Mod[] { new ManiaModMirror() }, 1], + [new Mod[] { new ManiaModDifficultyAdjust() }, 0.5], + [new Mod[] { new ManiaModClassic() }, 0.96], + [new Mod[] { new ManiaModInvert() }, 1], + [new Mod[] { new ManiaModConstantSpeed() }, 0.9], + [new Mod[] { new ManiaModHoldOff() }, 0.9], + [new Mod[] { new ManiaModKey1() }, 0.9], + [new Mod[] { new ManiaModKey2() }, 0.9], + [new Mod[] { new ManiaModKey3() }, 0.9], + [new Mod[] { new ManiaModKey4() }, 0.9], + [new Mod[] { new ManiaModKey5() }, 0.9], + [new Mod[] { new ManiaModKey6() }, 0.9], + [new Mod[] { new ManiaModKey7() }, 0.9], + [new Mod[] { new ManiaModKey8() }, 0.9], + [new Mod[] { new ManiaModKey9() }, 0.9], + [new Mod[] { new ManiaModKey10() }, 0.9], + + #endregion + + #region Automation + + [new Mod[] { new ManiaModAutoplay() }, 1], + [new Mod[] { new ManiaModCinema() }, 1], + + #endregion + + #region Fun + + [new Mod[] { new ModWindUp() }, 0.5], + [new Mod[] { new ModWindDown() }, 0.5], + [new Mod[] { new ManiaModMuted() }, 1], + [new Mod[] { new ModAdaptiveSpeed() }, 0.5], + + #endregion + + #region System + + [new Mod[] { new ManiaModScoreV2() }, 1], + + #endregion + + #region Combinations + + [new Mod[] { new ManiaModEasy(), new ManiaModKey4() }, 0.5 * 0.9] + + #endregion + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestMultipliers(Mod[] mods, double expectedMultiplier) + => TestModCombination(mods, expectedMultiplier); + + private static readonly object[][] key_mod_multiplier_test_cases = + [ + // score end date, client version, expected multiplier + + // scores verifiably from old clients. + [new DateTimeOffset(2024, 1, 31, 11, 0, 0, TimeSpan.Zero), "2024.130.2", 1], + [new DateTimeOffset(2024, 12, 9, 11, 0, 0, TimeSpan.Zero), "2024.1208.0", 1], + [new DateTimeOffset(2025, 6, 12, 11, 0, 0, TimeSpan.Zero), "2025.605.3", 1], + [new DateTimeOffset(2025, 6, 28, 11, 0, 0, TimeSpan.Zero), "2025.625.0-tachyon", 1], + [new DateTimeOffset(2025, 7, 11, 11, 0, 0, TimeSpan.Zero), "2025.710.0-lazer", 1], + [new DateTimeOffset(2025, 7, 15, 11, 0, 0, TimeSpan.Zero), "2025.711.0-tachyon", 1], + + // scores without explicit client versions, predating the change of multiplier. + // those MUST have used the old multiplier. + [new DateTimeOffset(2024, 1, 31, 11, 0, 0, TimeSpan.Zero), "", 1], + [new DateTimeOffset(2024, 12, 9, 11, 0, 0, TimeSpan.Zero), "", 1], + [new DateTimeOffset(2025, 6, 12, 11, 0, 0, TimeSpan.Zero), "", 1], + [new DateTimeOffset(2025, 6, 28, 11, 0, 0, TimeSpan.Zero), "", 1], + [new DateTimeOffset(2025, 7, 11, 11, 0, 0, TimeSpan.Zero), "", 1], + [new DateTimeOffset(2025, 7, 15, 11, 0, 0, TimeSpan.Zero), "", 1], + + // scores without explicit client versions, AFTER the change of multiplier. + // there is NO way of verifying whether these scores use the new or old multiplier, therefore GUESS that it's the new one. + // "thankfully" the window of opportunity for this occurring *should* be slim + // (from client release with new key mod multipliers on July 18, 2025 + // until spectator server release which added client version writing to server-side replays on August 1, 2025). + [new DateTimeOffset(2025, 7, 19, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9], + [new DateTimeOffset(2025, 7, 23, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9], + [new DateTimeOffset(2025, 8, 19, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9], + [new DateTimeOffset(2026, 6, 18, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9], + [new DateTimeOffset(2026, 7, 18, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9], + + // scores verifiably from new clients. + [new DateTimeOffset(2025, 7, 19, 0, 20, 15, 0, TimeSpan.Zero), "2025.718.0-tachyon", 0.9], + [new DateTimeOffset(2025, 7, 23, 0, 20, 15, 0, TimeSpan.Zero), "2025.721.0-tachyon", 0.9], + [new DateTimeOffset(2025, 8, 19, 0, 20, 15, 0, TimeSpan.Zero), "2025.816.0-lazer", 0.9], + [new DateTimeOffset(2026, 6, 18, 0, 20, 15, 0, TimeSpan.Zero), "2026.518.0-lazer", 0.9], + [new DateTimeOffset(2026, 7, 18, 0, 20, 15, 0, TimeSpan.Zero), "2026.522.1-tachyon", 0.9], + ]; + + [TestCaseSource(nameof(key_mod_multiplier_test_cases))] + public void TestKeyModMultiplierCompatibility(DateTimeOffset endDate, string clientVersion, double expectedMultiplier) + { + var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new ScoreInfo + { + Date = endDate, + ClientVersion = clientVersion + })); + Assert.That(calculator.CalculateFor([new ManiaModKey4()]), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON)); + } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs index 50ec119e4d..a77bfe35b1 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs @@ -8,8 +8,10 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests.Mods @@ -54,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods Mod = doubleTime, PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1 - && Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * doubleTime.ScoreMultiplier), + && Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * new ManiaScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor([doubleTime])), Autoplay = false, CreateBeatmap = () => new Beatmap { diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 18bb2b51ab..cd84b2b557 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Mania } } - public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ManiaScoreMultiplierCalculator(); + public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new ManiaScoreMultiplierCalculator(context); public override string Description => "osu!mania"; diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreMultiplierCalculator.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreMultiplierCalculator.cs index 22c77db6a3..d45c9a4d86 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreMultiplierCalculator.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreMultiplierCalculator.cs @@ -1,15 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { public class ManiaScoreMultiplierCalculator : ScoreMultiplierCalculator { - static ManiaScoreMultiplierCalculator() + public ManiaScoreMultiplierCalculator(ScoreMultiplierContext context) + : base(context) { #region Difficulty Reduction @@ -46,16 +49,16 @@ namespace osu.Game.Rulesets.Mania.Scoring // Invert Single(hasMultiplier: 0.9); Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); - Single(hasMultiplier: 0.9); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); + Single(hasMultiplier: keyModMultiplier(Context.Score)); #endregion @@ -95,5 +98,49 @@ namespace osu.Game.Rulesets.Mania.Scoring else return 0.6 + value; } + + private const double old_key_mod_multiplier = 1; + private const double new_key_mod_multiplier = 0.9; + + /// + /// + /// The mod multiplier was changed from 1.0x to 0.9x in https://github.com/ppy/osu/pull/30506 + /// which was included in the https://osu.ppy.sh/home/changelog/tachyon/2025.718.0 release. + /// The replay version was not bumped in the change, meaning that the only usable indicator + /// of the mod multiplier changing is the client version. + /// + /// + /// Unfortunately not even the client version is available on server-side recorded replays + /// recorded prior to https://github.com/ppy/osu-server-spectator/pull/290, + /// which does not appear to have been deployed until August 1 + /// (https://github.com/ppy/osu-server-spectator/releases/tag/2025.801.0). + /// + /// + private double keyModMultiplier(ScoreInfo? scoreInfo) + { + if (scoreInfo == null) + return new_key_mod_multiplier; + + string clientVersion = scoreInfo.ClientVersion; + + if (!string.IsNullOrEmpty(clientVersion)) + { + string[] pieces = clientVersion.Split('.'); + + if (int.TryParse(pieces[0], out int year) && int.TryParse(pieces[1], out int monthDay)) + { + if (year < 2025 || (year == 2025 && monthDay < 718)) + return old_key_mod_multiplier; + } + + return new_key_mod_multiplier; + } + + // Client version not available, fallback to doing the best we can with the score's timestamp. + if (scoreInfo.Date < new DateTimeOffset(2025, 7, 18, 0, 0, 0, TimeSpan.Zero)) + return old_key_mod_multiplier; + + return new_key_mod_multiplier; + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/OsuScoreMultiplierTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuScoreMultiplierTest.cs index 77e09f017f..49aa53d1af 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuScoreMultiplierTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuScoreMultiplierTest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Rulesets; @@ -14,32 +15,165 @@ namespace osu.Game.Rulesets.Osu.Tests { } - [Test] - public void TestFlashlightOnNonDefaultSettings() - => TestModCombination([new OsuModFlashlight { ComboBasedSize = { Value = false } }]); + private static readonly object[][] test_cases = + [ + #region Difficulty Reduction - [Test] - public void TestHiddenOnNonDefaultSettings() - => TestModCombination([new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }]); + [new Mod[] { new OsuModEasy() }, 0.5], + [new Mod[] { new OsuModNoFail() }, 0.5], - [Test] - public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new OsuModHalfTime { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new OsuModDaycore { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new OsuModDoubleTime { SpeedChange = { Value = speedChange } }]); + #endregion - [Test] - public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new OsuModNightcore { SpeedChange = { Value = speedChange } }]); + #region Difficulty Increase - [Test] - public void TestMultiplicativeCombination() - => TestModCombination([new OsuModHidden(), new OsuModHardRock()]); + [new Mod[] { new OsuModHardRock() }, 1.06], + [new Mod[] { new OsuModSuddenDeath() }, 1], + [new Mod[] { new OsuModPerfect() }, 1], + + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18], + [new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20], + + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18], + [new Mod[] { new OsuModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20], + + [new Mod[] { new OsuModHidden() }, 1.06], + [new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } } }, 1], + + [new Mod[] { new OsuModTraceable() }, 1], + + [new Mod[] { new OsuModFlashlight() }, 1.12], + [new Mod[] { new OsuModFlashlight { ComboBasedSize = { Value = false } } }, 1], + + [new Mod[] { new OsuModBlinds() }, 1.12], + [new Mod[] { new OsuModStrictTracking() }, 1], + [new Mod[] { new OsuModAccuracyChallenge() }, 1], + + #endregion + + #region Conversion + + [new Mod[] { new OsuModTargetPractice() }, 0.1], + [new Mod[] { new OsuModDifficultyAdjust() }, 0.5], + [new Mod[] { new OsuModClassic() }, 0.96], + [new Mod[] { new OsuModRandom() }, 1], + [new Mod[] { new OsuModMirror() }, 1], + [new Mod[] { new OsuModAlternate() }, 1], + [new Mod[] { new OsuModSingleTap() }, 1], + + #endregion + + #region Automation + + [new Mod[] { new OsuModAutoplay() }, 1], + [new Mod[] { new OsuModCinema() }, 1], + [new Mod[] { new OsuModRelax() }, 0.1], + [new Mod[] { new OsuModAutopilot() }, 0.1], + [new Mod[] { new OsuModSpunOut() }, 0.9], + + #endregion + + #region Fun + + [new Mod[] { new OsuModTransform() }, 1], + [new Mod[] { new OsuModWiggle() }, 1], + [new Mod[] { new OsuModSpinIn() }, 1], + [new Mod[] { new OsuModGrow() }, 1], + [new Mod[] { new OsuModDeflate() }, 1], + [new Mod[] { new ModWindUp() }, 0.5], + [new Mod[] { new ModWindDown() }, 0.5], + [new Mod[] { new OsuModBarrelRoll() }, 1], + [new Mod[] { new OsuModApproachDifferent() }, 1], + [new Mod[] { new OsuModMuted() }, 1], + [new Mod[] { new OsuModNoScope() }, 1], + [new Mod[] { new OsuModMagnetised() }, 0.5], + [new Mod[] { new OsuModRepel() }, 1], + [new Mod[] { new ModAdaptiveSpeed() }, 0.5], + [new Mod[] { new OsuModFreezeFrame() }, 1], + [new Mod[] { new OsuModBubbles() }, 1], + [new Mod[] { new OsuModSynesthesia() }, 0.8], + [new Mod[] { new OsuModDepth() }, 1], + [new Mod[] { new OsuModBloom() }, 1], + + #endregion + + #region System + + [new Mod[] { new OsuModTouchDevice() }, 1], + [new Mod[] { new ModScoreV2() }, 1], + + #endregion + + #region Combinations + + [new Mod[] { new OsuModHidden(), new OsuModHardRock() }, 1.06 * 1.06], + + #endregion + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestMultipliers(Mod[] mods, double expectedMultiplier) + => TestModCombination(mods, expectedMultiplier); } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index eb853e8996..9b4fbeddfd 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Osu } } - public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new OsuScoreMultiplierCalculator(); + public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new OsuScoreMultiplierCalculator(context); public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetOsu }; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreMultiplierCalculator.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreMultiplierCalculator.cs index 76b6a67180..9e59a0c2a2 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreMultiplierCalculator.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreMultiplierCalculator.cs @@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreMultiplierCalculator : ScoreMultiplierCalculator { - static OsuScoreMultiplierCalculator() + public OsuScoreMultiplierCalculator(ScoreMultiplierContext context) + : base(context) { #region Difficulty Reduction diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoScoreMultiplierTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoScoreMultiplierTest.cs index 53f8c043ac..b762de5ef4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoScoreMultiplierTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoScoreMultiplierTest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Rulesets; @@ -14,28 +15,143 @@ namespace osu.Game.Rulesets.Taiko.Tests { } - [Test] - public void TestFlashlightOnNonDefaultSettings() - => TestModCombination([new TaikoModFlashlight { ComboBasedSize = { Value = false } }]); + private static readonly object[][] test_cases = + [ + #region Difficulty Reduction - [Test] - public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new TaikoModHalfTime { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new TaikoModEasy() }, 0.5], + [new Mod[] { new TaikoModNoFail() }, 0.5], - [Test] - public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange) - => TestModCombination([new TaikoModDaycore { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new TaikoModDoubleTime { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5], + [new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5], - [Test] - public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange) - => TestModCombination([new TaikoModNightcore { SpeedChange = { Value = speedChange } }]); + [new Mod[] { new TaikoModSimplifiedRhythm() }, 0.6], - [Test] - public void TestMultiplicativeCombination() - => TestModCombination([new TaikoModHidden(), new TaikoModHardRock()]); + #endregion + + #region Difficulty Increase + + [new Mod[] { new TaikoModHardRock() }, 1.06], + [new Mod[] { new TaikoModSuddenDeath() }, 1], + [new Mod[] { new TaikoModPerfect() }, 1], + + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18], + [new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20], + + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18], + [new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20], + + [new Mod[] { new TaikoModHidden() }, 1.06], + + [new Mod[] { new TaikoModFlashlight() }, 1.12], + [new Mod[] { new TaikoModFlashlight { ComboBasedSize = { Value = false } } }, 1], + + [new Mod[] { new ModAccuracyChallenge() }, 1], + + #endregion + + #region Conversion + + [new Mod[] { new TaikoModRandom() }, 1], + [new Mod[] { new TaikoModDifficultyAdjust() }, 0.5], + [new Mod[] { new TaikoModClassic() }, 0.96], + [new Mod[] { new TaikoModSwap() }, 1], + [new Mod[] { new TaikoModSingleTap() }, 1], + [new Mod[] { new TaikoModConstantSpeed() }, 0.9], + + #endregion + + #region Automation + + [new Mod[] { new TaikoModAutoplay() }, 1], + [new Mod[] { new TaikoModCinema() }, 1], + [new Mod[] { new TaikoModRelax() }, 0.1], + + #endregion + + #region Fun + + [new Mod[] { new ModWindUp() }, 0.5], + [new Mod[] { new ModWindDown() }, 0.5], + [new Mod[] { new TaikoModMuted() }, 1], + [new Mod[] { new ModAdaptiveSpeed() }, 0.5], + + #endregion + + #region System + + [new Mod[] { new ModScoreV2() }, 1], + + #endregion + + #region Combinations + + [new Mod[] { new TaikoModHidden(), new TaikoModHardRock() }, 1.06 * 1.06] + + #endregion + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestMultipliers(Mod[] mods, double expectedMultiplier) + => TestModCombination(mods, expectedMultiplier); } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreMultiplierCalculator.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreMultiplierCalculator.cs index df5abc9832..244beaf8fd 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreMultiplierCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreMultiplierCalculator.cs @@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { public class TaikoScoreMultiplierCalculator : ScoreMultiplierCalculator { - static TaikoScoreMultiplierCalculator() + public TaikoScoreMultiplierCalculator(ScoreMultiplierContext context) + : base(context) { #region Difficulty Reduction diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 5aea2171b1..1220eaa70e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Taiko } } - public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new TaikoScoreMultiplierCalculator(); + public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new TaikoScoreMultiplierCalculator(context); public override string Description => "osu!taiko"; diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreMultiplierCalculatorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreMultiplierCalculatorTest.cs index 80644a08c9..b413755145 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreMultiplierCalculatorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreMultiplierCalculatorTest.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Tests.Rulesets.Scoring { @@ -12,7 +13,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [Test] public void TestFlatMultiplier() { - var calculator = new TestScoreMultiplierCalculator(); + var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext()); double multiplier = calculator.CalculateFor([new OsuModEasy()]); @@ -22,7 +23,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [Test] public void TestSettingDependentMultiplier() { - var calculator = new TestScoreMultiplierCalculator(); + var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext()); double multiplier = calculator.CalculateFor([new OsuModDaycore { SpeedChange = { Value = 0.6 } }]); @@ -32,17 +33,17 @@ namespace osu.Game.Tests.Rulesets.Scoring [Test] public void TestContextDependentMultiplier() { - var calculator = new TestScoreMultiplierCalculator(); + TestScoreMultiplierCalculator calculator; double multiplier; Assert.Multiple(() => { - calculator.HardRockPenalty = false; + calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext()); multiplier = calculator.CalculateFor([new OsuModHardRock()]); Assert.That(multiplier, Is.EqualTo(1.4)); - calculator.HardRockPenalty = true; + calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new ScoreInfo { ClientVersion = "2024.123.0" })); multiplier = calculator.CalculateFor([new OsuModHardRock()]); Assert.That(multiplier, Is.EqualTo(1.2)); }); @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [Test] public void TestCombinationMultiplier() { - var calculator = new TestScoreMultiplierCalculator(); + var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext()); double multiplier = calculator.CalculateFor([new OsuModEasy(), new OsuModDaycore()]); @@ -61,7 +62,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [Test] public void TestCombinationAndFlatMultipliers() { - var calculator = new TestScoreMultiplierCalculator(); + var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext()); double multiplier = calculator.CalculateFor([new OsuModDaycore(), new OsuModHardRock(), new OsuModEasy()]); @@ -70,15 +71,14 @@ namespace osu.Game.Tests.Rulesets.Scoring private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator { - static TestScoreMultiplierCalculator() + public TestScoreMultiplierCalculator(ScoreMultiplierContext context) + : base(context) { Single(hasMultiplier: 0.15); Single(hasMultiplier: daycore => (1 + daycore.SpeedChange.Value) / 4); - Single(hasMultiplier: (_, ctx) => ctx.HardRockPenalty ? 1.2 : 1.4); + Single(hasMultiplier: _ => context.Score?.ClientVersion == "2024.123.0" ? 1.2 : 1.4); Combination(hasMultiplier: (_, _) => 0.003); } - - public bool HardRockPenalty { get; set; } } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 65b3af50f3..f68cd9809e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -178,7 +178,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { LoadComponent(Overlay = new FreeModSelectOverlay { - SelectedMods = { BindTarget = FreeMods } + SelectedMods = { BindTarget = FreeMods }, + Ruleset = { BindTarget = Ruleset } }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFooterButtonMods.cs index 08d02015a2..2be921cb67 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFooterButtonMods.cs @@ -11,8 +11,11 @@ using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; using osu.Game.Utils; @@ -63,37 +66,45 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestIncrementMultiplier() { + var ruleset = new OsuRuleset(); var hiddenMod = new Mod[] { new OsuModHidden() }; + + AddStep("Set ruleset", () => footerButtonMods.Ruleset.Value = ruleset.RulesetInfo); + AddStep(@"Add Hidden", () => changeMods(hiddenMod)); - assertModsMultiplier(hiddenMod); + assertModsMultiplier(ruleset, hiddenMod); var hardRockMod = new Mod[] { new OsuModHardRock() }; AddStep(@"Add HardRock", () => changeMods(hardRockMod)); - assertModsMultiplier(hardRockMod); + assertModsMultiplier(ruleset, hardRockMod); var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); - assertModsMultiplier(doubleTimeMod); + assertModsMultiplier(ruleset, doubleTimeMod); var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); - assertModsMultiplier(multipleIncrementMods); + assertModsMultiplier(ruleset, multipleIncrementMods); } [Test] public void TestDecrementMultiplier() { + var ruleset = new OsuRuleset(); var easyMod = new Mod[] { new OsuModEasy() }; + + AddStep("Set ruleset", () => footerButtonMods.Ruleset.Value = ruleset.RulesetInfo); + AddStep(@"Add Easy", () => changeMods(easyMod)); - assertModsMultiplier(easyMod); + assertModsMultiplier(ruleset, easyMod); var noFailMod = new Mod[] { new OsuModNoFail() }; AddStep(@"Add NoFail", () => changeMods(noFailMod)); - assertModsMultiplier(noFailMod); + assertModsMultiplier(ruleset, noFailMod); var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); - assertModsMultiplier(multipleDecrementMods); + assertModsMultiplier(ruleset, multipleDecrementMods); } [Test] @@ -105,11 +116,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); } - private void changeMods(IReadOnlyList mods) => footerButtonMods.Current.Value = mods; + private void changeMods(IReadOnlyList mods) => footerButtonMods.Mods.Value = mods; - private void assertModsMultiplier(IEnumerable mods) + private void assertModsMultiplier(Ruleset ruleset, IEnumerable mods) { - double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); + double multiplier = scoreMultiplierCalculator.CalculateFor(mods); string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString(); AddAssert($"Displayed multiplier is {expectedValue}", () => footerButtonMods.ChildrenOfType().First(t => t.Text.ToString().Contains('x')).Text.ToString(), () => Is.EqualTo(expectedValue)); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b4f9365c8f..a13bdc7d43 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -25,6 +25,8 @@ using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Screens; using osu.Game.Screens.Footer; @@ -122,7 +124,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); AddAssert("mod multiplier correct", () => { - double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); + double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value); return Precision.AlmostEquals(multiplier, this.ChildrenOfType().Single().ModMultiplier.Value); }); assertCustomisationToggleState(disabled: false, active: false); @@ -137,7 +139,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); AddAssert("mod multiplier correct", () => { - double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); + double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value); return Precision.AlmostEquals(multiplier, this.ChildrenOfType().Single().ModMultiplier.Value); }); assertCustomisationToggleState(disabled: false, active: false); @@ -1087,6 +1089,7 @@ namespace osu.Game.Tests.Visual.UserInterface State = { Value = Visibility.Visible }, Beatmap = { Value = Beatmap.Value }, SelectedMods = { BindTarget = SelectedMods }, + Ruleset = { BindTarget = Ruleset }, ShowPresets = true, }); } diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 204198a410..6ab263e4a3 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -187,10 +187,9 @@ namespace osu.Game.Database break; } - double modMultiplier = 1; - - foreach (var mod in score.Mods) - modMultiplier *= mod.ScoreMultiplier; + var ruleset = score.Ruleset.CreateInstance(); + var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score)); + double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods); return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); @@ -352,7 +351,8 @@ namespace osu.Game.Database long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); - double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + var modMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); + double modMultiplier = modMultiplierCalculator.CalculateFor(score.Mods); long convertedTotalScoreWithoutMods; diff --git a/osu.Game/Overlays/Mods/ModSelectFooterContent.cs b/osu.Game/Overlays/Mods/ModSelectFooterContent.cs index 146b8e4ebe..a6b711aff1 100644 --- a/osu.Game/Overlays/Mods/ModSelectFooterContent.cs +++ b/osu.Game/Overlays/Mods/ModSelectFooterContent.cs @@ -10,7 +10,9 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Overlays.Mods @@ -28,6 +30,7 @@ namespace osu.Game.Overlays.Mods public readonly IBindable Beatmap = new Bindable(); public readonly IBindable> ActiveMods = new Bindable>(); + public readonly IBindable Ruleset = new Bindable(); /// /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown. @@ -101,6 +104,7 @@ namespace osu.Game.Overlays.Mods beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo; }, true); + Ruleset.BindValueChanged(_ => updateInformation()); ActiveMods.BindValueChanged(m => { updateInformation(); @@ -120,10 +124,8 @@ namespace osu.Game.Overlays.Mods { if (rankingInformationDisplay != null) { - double multiplier = 1.0; - - foreach (var mod in ActiveMods.Value) - multiplier *= mod.ScoreMultiplier; + var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); + double multiplier = scoreMultiplierCalculator?.CalculateFor(ActiveMods.Value) ?? 1; rankingInformationDisplay.ModMultiplier.Value = multiplier; rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9ba3b3774f..638fd74b47 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -26,6 +26,7 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Footer; using osu.Game.Utils; @@ -121,6 +122,7 @@ namespace osu.Game.Overlays.Mods private Sample? columnAppearSample; public readonly Bindable Beatmap = new Bindable(); + public readonly Bindable Ruleset = new Bindable(); [Resolved] private ScreenFooter? footer { get; set; } @@ -283,6 +285,7 @@ namespace osu.Game.Overlays.Mods { Beatmap = { BindTarget = Beatmap }, ActiveMods = { BindTarget = ActiveMods }, + Ruleset = { BindTarget = Ruleset }, }; private static readonly LocalisableString input_search_placeholder = Resources.Localisation.Web.CommonStrings.InputSearch; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index e7eaede892..a52a856b3c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -213,7 +213,7 @@ namespace osu.Game.Rulesets /// /// Creates a relevant to this ruleset. /// - public virtual ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ScoreMultiplierCalculator(); + public virtual ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new ScoreMultiplierCalculator(context); /// /// Create a transformer which adds lookups specific to a ruleset to skin sources. diff --git a/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs b/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs index 772f9d178b..ad481af2c9 100644 --- a/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs +++ b/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Scoring { @@ -13,47 +14,42 @@ namespace osu.Game.Rulesets.Scoring /// public class ScoreMultiplierCalculator { - private static readonly List<(Type[] mods, Func multiplier)> combination_multipliers = []; - private static readonly Dictionary> single_multipliers_with_context = []; - private static readonly Dictionary> single_multipliers = []; + protected ScoreMultiplierContext Context { get; } + + private readonly List<(Type[] mods, Func multiplier)> combinationMultipliers = []; + private readonly Dictionary> singleMultipliers = []; + + public ScoreMultiplierCalculator(ScoreMultiplierContext context) + { + Context = context; + } /// /// Defines a flat, setting-independent score multiplier for the given . /// - public static void Single(double hasMultiplier) + protected void Single(double hasMultiplier) where TMod : Mod { - single_multipliers[typeof(TMod)] = _ => hasMultiplier; + singleMultipliers[typeof(TMod)] = _ => hasMultiplier; } /// /// Defines a setting-dependent score multiplier for the given . /// - public static void Single(Func hasMultiplier) + protected void Single(Func hasMultiplier) where TMod : Mod { - single_multipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod); - } - - /// - /// Defines a setting-dependent score multiplier for the given . - /// The multiplier calculation is given additional context to calculate the multiplier via the type instance. - /// - public static void Single(Func hasMultiplier) - where TMod : Mod - where TContext : ScoreMultiplierCalculator - { - single_multipliers_with_context[typeof(TMod)] = (mod, context) => hasMultiplier.Invoke((TMod)mod, (TContext)context); + singleMultipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod); } /// /// Defines a score multiplier specific to when both and mods are present. /// - public static void Combination(Func hasMultiplier) + protected void Combination(Func hasMultiplier) where T1 : Mod where T2 : Mod { - combination_multipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1]))); + combinationMultipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1]))); } /// @@ -72,7 +68,7 @@ namespace osu.Game.Rulesets.Scoring if (allModsByType.Count > 1) { - foreach (var (combination, multiplier) in combination_multipliers) + foreach (var (combination, multiplier) in combinationMultipliers) { if (remainingModTypes.IsSupersetOf(combination)) { @@ -85,13 +81,48 @@ namespace osu.Game.Rulesets.Scoring foreach (var modType in remainingModTypes) { - if (single_multipliers.TryGetValue(modType, out var multiplier)) + if (singleMultipliers.TryGetValue(modType, out var multiplier)) result *= multiplier(allModsByType[modType]); - else if (single_multipliers_with_context.TryGetValue(modType, out var multiplierWithContext)) - result *= multiplierWithContext(allModsByType[modType], this); } return result; } } + + /// + /// Contextual information to pass to a + /// in order for it to calculate the correct multiplier. + /// + public class ScoreMultiplierContext + { + /// + /// The score that the multipliers are calculated for. + /// Mostly relevant and present in backwards compatibility scenarios. + /// In usages where the current valid score multipliers are required, pass or use a constructor that does not require this. + /// + public ScoreInfo? Score { get; } + + /// + /// Constructs a new instance. + /// Use this in situations wherein the current valid score multipliers are needed. + /// + public ScoreMultiplierContext() + : this(null) + { + } + + /// + /// Constructs a new instance. + /// Use this in backwards compatibility scenarios when dealing with a specific . + /// + /// + /// The score that the multipliers are calculated for. + /// Mostly relevant and present in backwards compatibility scenarios. + /// In usages where the current valid score multipliers are required, pass or use a constructor that does not require this. + /// + public ScoreMultiplierContext(ScoreInfo? score) + { + Score = score; + } + } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 3663e7f008..595ad6e524 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -206,10 +206,8 @@ namespace osu.Game.Rulesets.Scoring Mods.ValueChanged += mods => { - scoreMultiplier = 1; - - foreach (var m in mods.NewValue) - scoreMultiplier *= m.ScoreMultiplier; + var calculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); + scoreMultiplier = calculator.CalculateFor(mods.NewValue); updateScore(); updateRank(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a03fee5cfd..d79d8db73b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -258,10 +258,9 @@ namespace osu.Game.Scoring.Legacy public static void PopulateTotalScoreWithoutMods(ScoreInfo score) { - double modMultiplier = 1; - - foreach (var mod in score.Mods) - modMultiplier *= mod.ScoreMultiplier; + var ruleset = score.Ruleset.CreateInstance(); + var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score)); + double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods); score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier); } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 609d3fed80..478f878603 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -315,6 +315,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Beatmap = { BindTarget = Beatmap }, SelectedMods = { BindTarget = userMods }, + Ruleset = { BindTarget = Ruleset }, IsValidMod = _ => false }); diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 8937abb775..56315b9554 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay { Beatmap = { BindTarget = Beatmap }, ActiveMods = { BindTarget = ActiveMods }, + Ruleset = { BindTarget = Ruleset }, }; public partial class FreeModSelectFooterContent : ModSelectFooterContent diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index dcadbc493d..2ff8695380 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -291,7 +291,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ModSelectOverlay CreateModSelectOverlay() => modSelect = new UserModSelectOverlay(OverlayColourScheme.Plum) { - IsValidMod = isValidRequiredMod + IsValidMod = isValidRequiredMod, }; public override IReadOnlyList CreateFooterButtons() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 73cc126446..e8efb6b7be 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -421,7 +421,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer LoadComponent(userModsSelectOverlay = new MultiplayerUserModSelectOverlay { - Beatmap = { BindTarget = Beatmap } + Beatmap = { BindTarget = Beatmap }, + Ruleset = { BindTarget = Ruleset }, }); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 219bed9055..28e90f5cc7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -445,6 +445,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists SelectedItem = { BindTarget = SelectedItem }, SelectedMods = { BindTarget = UserMods }, Beatmap = { BindTarget = Beatmap }, + Ruleset = { BindTarget = Ruleset }, IsValidMod = _ => false }); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 2ab1deaf66..24d9610abc 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override ModSelectOverlay CreateModSelectOverlay() => modSelect = new UserModSelectOverlay(OverlayColourScheme.Plum) { - IsValidMod = isValidRequiredMod + IsValidMod = isValidRequiredMod, }; private PlaylistItem createItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) diff --git a/osu.Game/Screens/Select/BeatmapLeaderboardScore.Tooltip.cs b/osu.Game/Screens/Select/BeatmapLeaderboardScore.Tooltip.cs index 64a5f4334e..44ec842c4b 100644 --- a/osu.Game/Screens/Select/BeatmapLeaderboardScore.Tooltip.cs +++ b/osu.Game/Screens/Select/BeatmapLeaderboardScore.Tooltip.cs @@ -28,6 +28,7 @@ using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Utils; @@ -120,10 +121,9 @@ namespace osu.Game.Screens.Select var judgementsStatistics = value.GetStatisticsForDisplay().Select(s => new StatisticRow(s.DisplayName.ToUpper(), s.Count.ToLocalisableString("N0"), colours.ForHitResult(s.Result))); - double multiplier = 1.0; - - foreach (var mod in value.Mods) - multiplier *= mod.ScoreMultiplier; + var ruleset = value.Ruleset.CreateInstance(); + var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); + double multiplier = scoreMultiplierCalculator.CalculateFor(value.Mods); var generalStatistics = new[] { diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 81668cc414..1efa41bd91 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -22,7 +21,9 @@ using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Footer; using osu.Game.Screens.Play.HUD; using osu.Game.Utils; @@ -32,7 +33,7 @@ using osuTK.Input; namespace osu.Game.Screens.Select { - public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue> + public partial class FooterButtonMods : ScreenFooterButton { public Action? RequestDeselectAllMods { get; init; } @@ -40,12 +41,20 @@ namespace osu.Game.Screens.Select private const float mod_display_portion = 0.65f; - private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); + private readonly BindableWithCurrent> mods = new BindableWithCurrent>(Array.Empty()); - public Bindable> Current + public Bindable> Mods { - get => current.Current; - set => current.Current = value; + get => mods.Current; + set => mods.Current = value; + } + + private readonly BindableWithCurrent ruleset = new BindableWithCurrent(); + + public Bindable Ruleset + { + get => ruleset.Current; + set => ruleset.Current = value; } private Container modDisplayBar = null!; @@ -145,10 +154,10 @@ namespace osu.Game.Screens.Select Origin = Anchor.Centre, Shear = -OsuGame.SHEAR, Scale = new Vector2(0.5f), - Current = { BindTarget = Current }, + Current = { BindTarget = Mods }, ExpansionMode = ExpansionMode.AlwaysContracted, }, - overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Current }, }, + overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Mods }, }, } }, } @@ -165,7 +174,8 @@ namespace osu.Game.Screens.Select currentLanguage = game.CurrentLanguage.GetBoundCopy(); currentLanguage.BindValueChanged(_ => ScheduleAfterChildren(updateDisplay)); - Current.BindValueChanged(m => + Ruleset.BindValueChanged(_ => updateDisplay()); + Mods.BindValueChanged(m => { modSettingChangeTracker?.Dispose(); @@ -198,7 +208,7 @@ namespace osu.Game.Screens.Select private void updateDisplay() { - if (Current.Value.Count == 0) + if (Mods.Value.Count == 0) { modDisplayBar.MoveToY(20, duration, easing); modDisplayBar.FadeOut(duration, easing); @@ -213,7 +223,7 @@ namespace osu.Game.Screens.Select } else { - if (Current.Value.Any(m => !m.Ranked)) + if (Mods.Value.Any(m => !m.Ranked)) { unrankedBadge.MoveToX(0, duration, easing); unrankedBadge.FadeIn(duration, easing); @@ -234,7 +244,8 @@ namespace osu.Game.Screens.Select modDisplay.FadeIn(duration, easing); } - double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); + double multiplier = scoreMultiplierCalculator?.CalculateFor(Mods.Value) ?? 1; multiplierText.Text = ModUtils.FormatScoreMultiplier(multiplier); if (multiplier > 1) @@ -249,7 +260,7 @@ namespace osu.Game.Screens.Select { base.Update(); - if (Current.Value.Count == 0) + if (Mods.Value.Count == 0) return; if (modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b1df6a9be1..f03c9a5334 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -351,7 +351,8 @@ namespace osu.Game.Screens.Select new FooterButtonMods(modSelectOverlay) { Hotkey = GlobalAction.ToggleModSelection, - Current = Mods, + Mods = Mods, + Ruleset = Ruleset, RequestDeselectAllMods = () => { if (modSelectOverlay.State.Value == Visibility.Visible) @@ -708,6 +709,7 @@ namespace osu.Game.Screens.Select private void onArrivingAtScreen() { modSelectOverlay.Beatmap.BindTo(Beatmap); + modSelectOverlay.Ruleset.BindTo(Ruleset); // required due to https://github.com/ppy/osu-framework/issues/3218 modSelectOverlay.SelectedMods.Disabled = false; modSelectOverlay.SelectedMods.BindTo(Mods); @@ -755,6 +757,7 @@ namespace osu.Game.Screens.Select Beatmap.ValueChanged -= updateVariousState; modSelectOverlay.SelectedMods.UnbindFrom(Mods); + modSelectOverlay.Ruleset.UnbindFrom(Ruleset); modSelectOverlay.Beatmap.UnbindFrom(Beatmap); updateWedgeVisibility(); diff --git a/osu.Game/Tests/Rulesets/RulesetScoreMultiplierTest.cs b/osu.Game/Tests/Rulesets/RulesetScoreMultiplierTest.cs index fab314cdff..1c3e7b0d06 100644 --- a/osu.Game/Tests/Rulesets/RulesetScoreMultiplierTest.cs +++ b/osu.Game/Tests/Rulesets/RulesetScoreMultiplierTest.cs @@ -3,9 +3,10 @@ using System.Collections.Generic; using NUnit.Framework; -using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Utils; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Tests.Rulesets { @@ -21,33 +22,20 @@ namespace osu.Game.Tests.Rulesets [Test] public void TestDefaultMultiplierIsOne() - { - var calculator = Ruleset.CreateScoreMultiplierCalculator(); - Assert.That(calculator.CalculateFor([]), Is.EqualTo(1)); - } + => TestModCombination([], 1); - [Test] - public void TestMultipliersMatchForIndividualMods() + protected void TestModCombination(IEnumerable mods, double expectedMultiplier) { - var mods = Ruleset.CreateAllMods(); - var calculator = Ruleset.CreateScoreMultiplierCalculator(); - Assert.Multiple(() => { + double multiplierViaOldAPI = 1; foreach (var mod in mods) - Assert.That(calculator.CalculateFor(mod.Yield()), Is.EqualTo(mod.ScoreMultiplier), message: $"Score multiplier not matching for mod {mod.Name}"); + multiplierViaOldAPI *= mod.ScoreMultiplier; + Assert.That(multiplierViaOldAPI, Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON)); + + var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext()); + Assert.That(calculator.CalculateFor(mods), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON)); }); } - - protected void TestModCombination(IEnumerable mods) - { - var calculator = Ruleset.CreateScoreMultiplierCalculator(); - - double expected = 1; - foreach (var mod in mods) - expected *= mod.ScoreMultiplier; - - Assert.That(calculator.CalculateFor(mods), Is.EqualTo(expected)); - } } }