From 9727d95ad9a46ecaafd8b4fe62e68f45db607661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 May 2026 11:02:15 +0200 Subject: [PATCH] Replace usages of `Mod.ScoreMultiplier` with new score multiplier API (#37845) - Part of https://github.com/ppy/osu/issues/37818 During review, I would like to direct particular attention to the following changes: ## [Migrate song select to new score multiplier API](https://github.com/ppy/osu/commit/945fd78539da3ae57d1550a5bbfb0f859d153cc4) This was a confusing change to write because of the way song selects hook their mod overlays up to global bindables. In particular different things happen in different circumstances. - When going through `SongSelect.CreateModOverlay()`, which is called by the base `SongSelect`, the mod overlay is automatically bound to global bindables via `SongSelect.on{ArrivingAt,Leaving}Screen()`. - For multiplayer user mod select overlays, which are bolted on by subclasses of `SongSelect`, manual hook-up is required. - As for free mod select overlays, they don't show mod multipliers at all, and don't have easy access to the ruleset, and thus the hookup is skipped entirely as redundant. ## [Fix score multiplier registrations being shared between implementations via superclass static fields](https://github.com/ppy/osu/commit/ba0a7ad421e0c84c2d8162b6bbdd3a0683f5a6a6) Revealed by `ScoreMultiplierCalculatorTest` starting to fail due to interference from `OsuScoreMultiplierCalculator`. It's not ideal from a performance standpoint but it's the simplest choice for now. Tricks could be pulled to salvage the static. One is ```csharp public class ScoreMultiplierCalculator where T : ScoreMultiplierCalculator { } ``` This works because of generics internals; static instance members are not shared between different specialisations of a generic class. It is also very unintuitive, so I would rather not. (It trips a ReSharper inspection too, which would have to be silenced.) From a performance standpoint this is not ideal, but a significant chunk of migrated usages already precede the construction of the calculator via the known-expensive `RulesetInfo.CreateInstance()`, and the paths that actually construct the calculator do not appear to be that hot. If need be, this can be handled by actually caching ruleset instances and their derivative subcomponents. ## [Introduce passing of context to score multiplier calculator](https://github.com/ppy/osu/pull/37845/changes/9e9242b3221dddacd226f4b3b9c5632d7350e998) This is required for two reasons: - The upcoming mod rebalance will require out-of-band supplementary information that is not available for reading from the mod instances themselves for calculating the multiplier. - This context, namely passing of `ScoreInfo`, will be used for implementing backwards compatibility with old scores and their score multipliers. This is required because it has turned out under inspection that all server-side lazer replays recorded until now are missing `TotalScoreWithoutMods` due to an omission of not sending it across the wire to spectator server. Because the score import flow uses replays, filtered through `LegacyScoreDecoder`, to populate total score in the realm database, it is basically impossible to ignore scores that are missing `TotalScoreWithoutMods`, because that will result in bug reports that the scores do not have the new score multipliers applied. Thus, passing of `ScoreInfo` will facilitate implementation of versioning score multipliers, which should result in less breakage than not doing so. An example of this is added in 341b2d6e55abb186ee84e66057fc029eed0a22ba, which should handle the case of mania mod multipliers having been changed without any attempt to facilitate for it in https://github.com/ppy/osu/pull/30506. --------- Co-authored-by: Dean Herbert --- .../BenchmarkScoreMultiplierCalculator.cs | 24 +- .../CatchScoreMultiplierTest.cs | 150 ++++++++++-- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Scoring/CatchScoreMultiplierCalculator.cs | 3 +- .../ManiaScoreMultiplierTest.cs | 216 ++++++++++++++++-- .../Mods/TestSceneManiaModDoubleTime.cs | 4 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Scoring/ManiaScoreMultiplierCalculator.cs | 69 +++++- .../OsuScoreMultiplierTest.cs | 176 ++++++++++++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Scoring/OsuScoreMultiplierCalculator.cs | 3 +- .../TaikoScoreMultiplierTest.cs | 152 ++++++++++-- .../Scoring/TaikoScoreMultiplierCalculator.cs | 3 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- .../Scoring/ScoreMultiplierCalculatorTest.cs | 22 +- .../TestSceneFreeModSelectOverlay.cs | 3 +- .../SongSelect/TestSceneFooterButtonMods.cs | 32 ++- .../TestSceneModSelectOverlay.cs | 7 +- .../StandardisedScoreMigrationTools.cs | 10 +- .../Overlays/Mods/ModSelectFooterContent.cs | 10 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 + osu.Game/Rulesets/Ruleset.cs | 2 +- .../Scoring/ScoreMultiplierCalculator.cs | 79 +++++-- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 +- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 +- .../DailyChallenge/DailyChallenge.cs | 1 + .../OnlinePlay/FreeModSelectOverlay.cs | 1 + .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 3 +- .../Playlists/PlaylistsRoomSubScreen.cs | 1 + .../Playlists/PlaylistsSongSelect.cs | 2 +- .../Select/BeatmapLeaderboardScore.Tooltip.cs | 8 +- osu.Game/Screens/Select/FooterButtonMods.cs | 37 +-- osu.Game/Screens/Select/SongSelect.cs | 5 +- .../Rulesets/RulesetScoreMultiplierTest.cs | 32 +-- 35 files changed, 858 insertions(+), 223 deletions(-) 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)); - } } }