Skip to content

Commit 45234b5

Browse files
tsunyokuBartłomiej Dach
andauthored
Add beatmap difficulty before mods as context for score multiplier calculations (ppy#37921)
- Part of ppy#37818 Access to difficulty info is required for the upcoming multiplier proposals. All places providing difficulty info intentionally use `IBeatmapInfo` as the difficulty info exposed to the calculator should _always_ be pre-mods for our usecase. There's a couple of quirks: - The usage in `ScoreProcessor` is a bit troubling to me but I can't see a way to make it better without refactoring it. Essentially, we don't have a beatmap until `ApplyBeatmap` is called, but most usages of `ScoreProcessor` are setting `Mods` prior to `ApplyBeatmap` so there is a `null` check in the logic for when mods change. Additionally, this means a new bindable of the beatmap via `ApplyBeatmap` which also feels a bit dirty. Open to suggestions. - ~~`BeatmapLeaderboardScore.Tooltip` is using a null-forgiving on the `BeatmapInfo`, but there's basically no context available on if this is an issue - the only code path which sets the score is `SetContent` which has no callers, so it's essentially dead code. Makes sense given it's Select V1.~~ --------- Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
1 parent 926f1c9 commit 45234b5

15 files changed

Lines changed: 137 additions & 37 deletions

File tree

osu.Game.Benchmarks/BenchmarkScoreMultiplierCalculator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using BenchmarkDotNet.Attributes;
66
using NUnit.Framework;
7+
using osu.Game.Beatmaps;
78
using osu.Game.Rulesets.Mods;
89
using osu.Game.Rulesets.Osu;
910
using osu.Game.Rulesets.Osu.Mods;
@@ -42,7 +43,7 @@ public record ModTestCase(string Description, IEnumerable<Mod> Mods)
4243
public override void SetUp()
4344
{
4445
base.SetUp();
45-
calculator = new OsuRuleset().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
46+
calculator = new OsuRuleset().CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
4647
}
4748

4849
[Benchmark]

osu.Game.Rulesets.Mania.Tests/ManiaScoreMultiplierTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using NUnit.Framework;
66
using osu.Framework.Utils;
7+
using osu.Game.Beatmaps;
78
using osu.Game.Rulesets.Mania.Mods;
89
using osu.Game.Rulesets.Mods;
910
using osu.Game.Rulesets.Scoring;
@@ -212,7 +213,7 @@ [new DateTimeOffset(2026, 7, 18, 0, 20, 15, 0, TimeSpan.Zero), "2026.522.1-tachy
212213
[TestCaseSource(nameof(key_mod_multiplier_test_cases))]
213214
public void TestKeyModMultiplierCompatibility(DateTimeOffset endDate, string clientVersion, double expectedMultiplier)
214215
{
215-
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new ScoreInfo
216+
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty(), new ScoreInfo
216217
{
217218
Date = endDate,
218219
ClientVersion = clientVersion

osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void TestHitWindowWithDoubleTime()
5656
Mod = doubleTime,
5757
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
5858
&& Player.ScoreProcessor.Accuracy.Value == 1
59-
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * new ManiaScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor([doubleTime])),
59+
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * new ManiaScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty())).CalculateFor([doubleTime])),
6060
Autoplay = false,
6161
CreateBeatmap = () => new Beatmap
6262
{

osu.Game.Tests/Rulesets/Scoring/ScoreMultiplierCalculatorTest.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using NUnit.Framework;
5+
using osu.Game.Beatmaps;
56
using osu.Game.Rulesets.Osu.Mods;
67
using osu.Game.Rulesets.Scoring;
78
using osu.Game.Scoring;
@@ -13,7 +14,7 @@ public class ScoreMultiplierCalculatorTest
1314
[Test]
1415
public void TestFlatMultiplier()
1516
{
16-
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
17+
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
1718

1819
double multiplier = calculator.CalculateFor([new OsuModEasy()]);
1920

@@ -23,36 +24,55 @@ public void TestFlatMultiplier()
2324
[Test]
2425
public void TestSettingDependentMultiplier()
2526
{
26-
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
27+
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
2728

2829
double multiplier = calculator.CalculateFor([new OsuModDaycore { SpeedChange = { Value = 0.6 } }]);
2930

3031
Assert.That(multiplier, Is.EqualTo(0.4));
3132
}
3233

3334
[Test]
34-
public void TestContextDependentMultiplier()
35+
public void TestScoreDependentMultiplier()
3536
{
3637
TestScoreMultiplierCalculator calculator;
3738

3839
double multiplier;
3940

4041
Assert.Multiple(() =>
4142
{
42-
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
43+
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
4344
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
4445
Assert.That(multiplier, Is.EqualTo(1.4));
4546

46-
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new ScoreInfo { ClientVersion = "2024.123.0" }));
47+
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty(), new ScoreInfo { ClientVersion = "2024.123.0" }));
4748
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
4849
Assert.That(multiplier, Is.EqualTo(1.2));
4950
});
5051
}
5152

53+
[Test]
54+
public void TestDifficultyDependentMultiplier()
55+
{
56+
TestScoreMultiplierCalculator calculator;
57+
58+
double multiplier;
59+
60+
Assert.Multiple(() =>
61+
{
62+
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
63+
multiplier = calculator.CalculateFor([new OsuModEasy()]);
64+
Assert.That(multiplier, Is.EqualTo(0.15));
65+
66+
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty { ApproachRate = 0 }));
67+
multiplier = calculator.CalculateFor([new OsuModEasy()]);
68+
Assert.That(multiplier, Is.EqualTo(0.1));
69+
});
70+
}
71+
5272
[Test]
5373
public void TestCombinationMultiplier()
5474
{
55-
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
75+
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
5676

5777
double multiplier = calculator.CalculateFor([new OsuModEasy(), new OsuModDaycore()]);
5878

@@ -62,7 +82,7 @@ public void TestCombinationMultiplier()
6282
[Test]
6383
public void TestCombinationAndFlatMultipliers()
6484
{
65-
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
85+
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
6686

6787
double multiplier = calculator.CalculateFor([new OsuModDaycore(), new OsuModHardRock(), new OsuModEasy()]);
6888

@@ -74,7 +94,7 @@ private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator
7494
public TestScoreMultiplierCalculator(ScoreMultiplierContext context)
7595
: base(context)
7696
{
77-
Single<OsuModEasy>(hasMultiplier: 0.15);
97+
Single<OsuModEasy>(hasMultiplier: context.BeatmapDifficultyWithoutMods.ApproachRate == 0 ? 0.1 : 0.15);
7898
Single<OsuModDaycore>(hasMultiplier: daycore => (1 + daycore.SpeedChange.Value) / 4);
7999
Single<OsuModHardRock>(hasMultiplier: _ => context.Score?.ClientVersion == "2024.123.0" ? 1.2 : 1.4);
80100
Combination<OsuModEasy, OsuModDaycore>(hasMultiplier: (_, _) => 0.003);

osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,29 @@ public void TestComboAccounting([Values] bool shuffleResults)
481481
Assert.That(scoreProcessor.HighestCombo.Value, Is.Zero);
482482
}
483483

484+
[Test]
485+
public void TestScoreMultiplier()
486+
{
487+
Mod[] mods = new Mod[] { new OsuModHardRock() };
488+
489+
scoreProcessor = new TestScoreProcessor();
490+
scoreProcessor.Mods.Value = mods;
491+
492+
var workingBeatmap = new TestWorkingBeatmap(beatmap);
493+
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, mods);
494+
495+
scoreProcessor.ApplyBeatmap(playableBeatmap);
496+
497+
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
498+
{
499+
Type = HitResult.Great,
500+
};
501+
scoreProcessor.ApplyResult(judgementResult);
502+
503+
Assert.That(scoreProcessor.MaximumTotalScore, Is.EqualTo(1_000_000 * 1.1).Within(0.5d));
504+
Assert.That(scoreProcessor.GetDisplayScore(ScoringMode.Standardised), Is.EqualTo(1_000_000 * 1.1).Within(0.5d));
505+
}
506+
484507
private class TestJudgement : Judgement
485508
{
486509
public override HitResult MaxResult { get; }
@@ -537,9 +560,20 @@ private class TestRuleset : Ruleset
537560

538561
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
539562

563+
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new TestScoreMultiplierCalculator(context);
564+
540565
public override string Description => string.Empty;
541566
public override string ShortName => string.Empty;
542567
}
568+
569+
private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator
570+
{
571+
public TestScoreMultiplierCalculator(ScoreMultiplierContext context)
572+
: base(context)
573+
{
574+
Single<OsuModHardRock>(hasMultiplier: context.BeatmapDifficultyWithoutMods.CircleSize == 4 ? 1.1 : 1.0);
575+
}
576+
}
543577
}
544578
}
545579
}

osu.Game.Tests/Visual/SongSelect/TestSceneFooterButtonMods.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using osu.Framework.Allocation;
99
using osu.Framework.Graphics;
1010
using osu.Framework.Testing;
11+
using osu.Game.Beatmaps;
1112
using osu.Game.Graphics.Sprites;
1213
using osu.Game.Overlays;
1314
using osu.Game.Overlays.Mods;
@@ -120,7 +121,7 @@ public void TestUnrankedBadge()
120121

121122
private void assertModsMultiplier(Ruleset ruleset, IEnumerable<Mod> mods)
122123
{
123-
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
124+
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
124125
double multiplier = scoreMultiplierCalculator.CalculateFor(mods);
125126
string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString();
126127

osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using osu.Framework.Screens;
1616
using osu.Framework.Testing;
1717
using osu.Framework.Utils;
18+
using osu.Game.Beatmaps;
1819
using osu.Game.Configuration;
1920
using osu.Game.Graphics.UserInterface;
2021
using osu.Game.Overlays;
@@ -124,7 +125,7 @@ public void TestPreexistingSelection()
124125
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
125126
AddAssert("mod multiplier correct", () =>
126127
{
127-
double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value);
128+
double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty())).CalculateFor(SelectedMods.Value);
128129
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
129130
});
130131
assertCustomisationToggleState(disabled: false, active: false);
@@ -139,7 +140,7 @@ public void TestExternalSelection()
139140
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
140141
AddAssert("mod multiplier correct", () =>
141142
{
142-
double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value);
143+
double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty())).CalculateFor(SelectedMods.Value);
143144
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
144145
});
145146
assertCustomisationToggleState(disabled: false, active: false);

osu.Game/Database/StandardisedScoreMigrationTools.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,10 @@ public static long GetOldStandardised(ScoreInfo score)
188188
}
189189

190190
var ruleset = score.Ruleset.CreateInstance();
191-
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score));
191+
192+
Debug.Assert(score.BeatmapInfo != null);
193+
194+
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score.BeatmapInfo.Difficulty, score));
192195
double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods);
193196

194197
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
@@ -351,7 +354,7 @@ private static (long withoutMods, long withMods) convertFromLegacyTotalScore(Sco
351354
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
352355
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
353356

354-
var modMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
357+
var modMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(difficulty));
355358
double modMultiplier = modMultiplierCalculator.CalculateFor(score.Mods);
356359

357360
long convertedTotalScoreWithoutMods;

osu.Game/Overlays/Mods/ModSelectFooterContent.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ protected override void LoadComplete()
102102
{
103103
if (beatmapAttributesDisplay != null)
104104
beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo;
105+
106+
updateInformation();
105107
}, true);
106108

107109
Ruleset.BindValueChanged(_ => updateInformation());
@@ -122,9 +124,11 @@ protected override void LoadComplete()
122124

123125
private void updateInformation()
124126
{
125-
if (rankingInformationDisplay != null)
127+
WorkingBeatmap? workingBeatmap = Beatmap.Value;
128+
129+
if (rankingInformationDisplay != null && workingBeatmap != null)
126130
{
127-
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
131+
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext(workingBeatmap.BeatmapInfo.Difficulty));
128132
double multiplier = scoreMultiplierCalculator?.CalculateFor(ActiveMods.Value) ?? 1;
129133

130134
rankingInformationDisplay.ModMultiplier.Value = multiplier;

osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using osu.Game.Beatmaps;
78
using osu.Game.Rulesets.Mods;
89
using osu.Game.Scoring;
910

@@ -95,6 +96,12 @@ public double CalculateFor(IEnumerable<Mod> mods)
9596
/// </summary>
9697
public class ScoreMultiplierContext
9798
{
99+
/// <summary>
100+
/// The difficulty info for the beatmap that the multipliers are calculated for.
101+
/// This must be the difficulty info for the beatmap BEFORE any mod application.
102+
/// </summary>
103+
public IBeatmapDifficultyInfo BeatmapDifficultyWithoutMods { get; }
104+
98105
/// <summary>
99106
/// The score that the multipliers are calculated for.
100107
/// Mostly relevant and present in backwards compatibility scenarios.
@@ -104,24 +111,19 @@ public class ScoreMultiplierContext
104111

105112
/// <summary>
106113
/// Constructs a new instance.
107-
/// Use this in situations wherein the current valid score multipliers are needed.
108-
/// </summary>
109-
public ScoreMultiplierContext()
110-
: this(null)
111-
{
112-
}
113-
114-
/// <summary>
115-
/// Constructs a new instance.
116-
/// Use this in backwards compatibility scenarios when dealing with a specific <paramref name="score"/>.
117114
/// </summary>
115+
/// <param name="beatmapDifficultyWithoutMods">
116+
/// The difficulty info for the beatmap that the multipliers are calculated for.
117+
/// This must be the difficulty info for the beatmap BEFORE any mod application.
118+
/// </param>
118119
/// <param name="score">
119120
/// The score that the multipliers are calculated for.
120121
/// Mostly relevant and present in backwards compatibility scenarios.
121-
/// In usages where the current valid score multipliers are required, pass <see langword="null"/> or use a constructor that does not require this.
122+
/// In usages where the current valid score multipliers are required, pass <see langword="null"/> or omit this parameter entirely.
122123
/// </param>
123-
public ScoreMultiplierContext(ScoreInfo? score)
124+
public ScoreMultiplierContext(IBeatmapDifficultyInfo beatmapDifficultyWithoutMods, ScoreInfo? score = null)
124125
{
126+
BeatmapDifficultyWithoutMods = beatmapDifficultyWithoutMods;
125127
Score = score;
126128
}
127129
}

0 commit comments

Comments
 (0)