From 2c382bd1d9acdad5fa0e9ba4d80f996c36d5044e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 11:26:07 +0900 Subject: [PATCH 1/7] Rename GetImmediateScore() as overload of GetScore() --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 2 +- .../PerformanceBreakdownCalculator.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 46 +++++++++---------- .../HUD/MultiplayerGameplayLeaderboard.cs | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index c6e7988543..e96ff1f7f1 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -300,7 +300,7 @@ namespace osu.Game.Tests.Rulesets.Scoring HitObjects = { new TestHitObject(result) } }); - Assert.That(scoreProcessor.GetImmediateScore(ScoringMode.Standardised, result.AffectsCombo() ? 1 : 0, statistic), Is.EqualTo(expectedScore).Within(0.5d)); + Assert.That(scoreProcessor.GetScore(ScoringMode.Standardised, result.AffectsCombo() ? 1 : 0, statistic), Is.EqualTo(expectedScore).Within(0.5d)); } private class TestJudgement : Judgement diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 3d384f5914..dfb8066b28 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Difficulty ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo; scoreProcessor.Mods.Value = perfectPlay.Mods; - perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); + perfectPlay.TotalScore = (long)scoreProcessor.GetScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); // compute rank achieved // default to SS, then adjust the rank with mods diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index d5a5aa4592..af9b4db5ca 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -218,6 +218,29 @@ namespace osu.Game.Rulesets.Scoring scoreResultCounts); } + /// + /// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time. + /// + /// The to compute the total score in. + /// The maximum combo achievable in the beatmap. + /// Statistics to be used for calculating accuracy, bonus score, etc. + /// The computed score for provided inputs. + public double GetScore(ScoringMode mode, int maxCombo, Dictionary statistics) + { + // calculate base score from statistics pairs + int computedBaseScore = 0; + + foreach (var pair in statistics) + { + if (!pair.Key.AffectsAccuracy()) + continue; + + computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; + } + + return GetScore(mode, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), statistics); + } + /// /// Computes the total score. /// @@ -250,29 +273,6 @@ namespace osu.Game.Rulesets.Scoring } } - /// - /// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time. - /// - /// The to compute the total score in. - /// The maximum combo achievable in the beatmap. - /// Statistics to be used for calculating accuracy, bonus score, etc. - /// The computed score for provided inputs. - public double GetImmediateScore(ScoringMode mode, int maxCombo, Dictionary statistics) - { - // calculate base score from statistics pairs - int computedBaseScore = 0; - - foreach (var pair in statistics) - { - if (!pair.Key.AffectsAccuracy()) - continue; - - computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; - } - - return GetScore(mode, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), statistics); - } - /// /// Get the accuracy fraction for the provided base score. /// diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 83c73e5a70..0516e00b8b 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -244,7 +244,7 @@ namespace osu.Game.Screens.Play.HUD { var header = frame.Header; - Score.Value = ScoreProcessor.GetImmediateScore(ScoringMode.Value, header.MaxCombo, header.Statistics); + Score.Value = ScoreProcessor.GetScore(ScoringMode.Value, header.MaxCombo, header.Statistics); Accuracy.Value = header.Accuracy; CurrentCombo.Value = header.Combo; } From 6654977a7b73c19d2250b030e519055c8be7cbae Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 11:28:14 +0900 Subject: [PATCH 2/7] Add GetScore() overload with total hitobject count --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index af9b4db5ca..67b78d5230 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -250,6 +250,17 @@ namespace osu.Game.Rulesets.Scoring /// Any statistics to be factored in. /// The total score. public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, Dictionary statistics) + { + int totalHitObjects = statistics.Where(k => k.Key >= HitResult.Miss && k.Key <= HitResult.Perfect).Sum(k => k.Value); + + // If there are no hitobjects then the beatmap can be composed of only ticks or spinners, so ensure we don't multiply by 0 at all times. + if (totalHitObjects == 0) + totalHitObjects = 1; + + return GetScore(mode, accuracyRatio, comboRatio, statistics, totalHitObjects); + } + + public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, Dictionary statistics, int totalHitObjects) { switch (mode) { @@ -260,12 +271,6 @@ namespace osu.Game.Rulesets.Scoring return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; case ScoringMode.Classic: - int totalHitObjects = statistics.Where(k => k.Key >= HitResult.Miss && k.Key <= HitResult.Perfect).Sum(k => k.Value); - - // If there are no hitobjects then the beatmap can be composed of only ticks or spinners, so ensure we don't multiply by 0 at all times. - if (totalHitObjects == 0) - totalHitObjects = 1; - // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score; From 5b6b8d1fa95c32f10500b838b95cf116dbe59abf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 11:29:47 +0900 Subject: [PATCH 3/7] Remove GetStandardisedScore() proxy method --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 14 +++----------- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 3 ++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 67b78d5230..422b29601a 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -207,16 +207,10 @@ namespace osu.Game.Rulesets.Scoring if (rollingMaxBaseScore != 0) Accuracy.Value = calculateAccuracyRatio(baseScore, true); - TotalScore.Value = getScore(Mode.Value); + TotalScore.Value = GetScore(Mode.Value); } - private double getScore(ScoringMode mode) - { - return GetScore(mode, - calculateAccuracyRatio(baseScore), - calculateComboRatio(HighestCombo.Value), - scoreResultCounts); - } + public double GetScore(ScoringMode mode) => GetScore(mode, calculateAccuracyRatio(baseScore), calculateComboRatio(HighestCombo.Value), scoreResultCounts); /// /// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time. @@ -316,8 +310,6 @@ namespace osu.Game.Rulesets.Scoring public int GetStatistic(HitResult result) => scoreResultCounts.GetValueOrDefault(result); - public double GetStandardisedScore() => getScore(ScoringMode.Standardised); - /// /// Resets this ScoreProcessor to a default state. /// @@ -351,7 +343,7 @@ namespace osu.Game.Rulesets.Scoring /// public virtual void PopulateScore(ScoreInfo score) { - score.TotalScore = (long)Math.Round(GetStandardisedScore()); + score.TotalScore = (long)Math.Round(GetScore(ScoringMode.Standardised)); score.Combo = Combo.Value; score.MaxCombo = HighestCombo.Value; score.Accuracy = Accuracy.Value; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 7efeae8129..77eaf133c7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -11,6 +11,7 @@ using osu.Framework.Screens; using osu.Game.Extensions; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -64,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetScore(ScoringMode.Standardised)); } protected override void Dispose(bool isDisposing) From a8e99f1a9521bdc528aaf66817183bc5f1e6b085 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 11:45:04 +0900 Subject: [PATCH 4/7] Calculate classic score using total basic hitobject count --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 93 +++++++++++++++------ 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 422b29601a..b0656e270e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; @@ -86,9 +87,22 @@ namespace osu.Game.Rulesets.Scoring /// private double maxBaseScore; + /// + /// The maximum number of basic (non-tick and non-bonus) hitobjects. + /// + private int maxBasicHitObjects; + + /// + /// Maximum for a normal hit (i.e. not tick/bonus) for this ruleset. + /// Only populated via . + /// + private HitResult? maxBasicHitResult; + private double rollingMaxBaseScore; private double baseScore; + private int basicHitObjects; + private readonly Dictionary scoreResultCounts = new Dictionary(); private readonly List hitEvents = new List(); private HitObject lastHitObject; @@ -122,8 +136,6 @@ namespace osu.Game.Rulesets.Scoring }; } - private readonly Dictionary scoreResultCounts = new Dictionary(); - protected sealed override void ApplyResultInternal(JudgementResult result) { result.ComboAtJudgement = Combo.Value; @@ -160,6 +172,9 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore += result.Judgement.MaxNumericResult; } + if (result.Type.IsBasic()) + basicHitObjects++; + hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; @@ -195,6 +210,9 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } + if (result.Type.IsBasic()) + basicHitObjects--; + Debug.Assert(hitEvents.Count > 0); lastHitObject = hitEvents[^1].LastHitObject; hitEvents.RemoveAt(hitEvents.Count - 1); @@ -210,15 +228,30 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = GetScore(Mode.Value); } - public double GetScore(ScoringMode mode) => GetScore(mode, calculateAccuracyRatio(baseScore), calculateComboRatio(HighestCombo.Value), scoreResultCounts); + /// + /// Computes the total score from judgements that have been applied to this + /// through and . + /// + /// + /// Requires an to have been applied via before use. + /// + /// The to represent the score as. + /// The total score in the given . + public double GetScore(ScoringMode mode) + { + return GetScore(mode, calculateAccuracyRatio(baseScore), calculateComboRatio(HighestCombo.Value), scoreResultCounts); + } /// - /// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time. + /// Computes the total score from judgements counts in a statistics dictionary. /// - /// The to compute the total score in. + /// + /// Requires an to have been applied via before use. + /// + /// The to represent the score as. /// The maximum combo achievable in the beatmap. - /// Statistics to be used for calculating accuracy, bonus score, etc. - /// The computed score for provided inputs. + /// The statistics to compute the score for. + /// The total score computed from judgements in the statistics dictionary. public double GetScore(ScoringMode mode, int maxCombo, Dictionary statistics) { // calculate base score from statistics pairs @@ -236,25 +269,34 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Computes the total score. + /// Computes the total score from given scoring component ratios. /// - /// The to compute the total score in. + /// + /// Requires an to have been applied via before use. + /// + /// The to represent the score as. /// The accuracy percentage achieved by the player. - /// The proportion of the max combo achieved by the player. - /// Any statistics to be factored in. - /// The total score. + /// The portion of the max combo achieved by the player. + /// Any additional statistics to be factored in. + /// The total score computed from the given scoring component ratios. public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, Dictionary statistics) { - int totalHitObjects = statistics.Where(k => k.Key >= HitResult.Miss && k.Key <= HitResult.Perfect).Sum(k => k.Value); - - // If there are no hitobjects then the beatmap can be composed of only ticks or spinners, so ensure we don't multiply by 0 at all times. - if (totalHitObjects == 0) - totalHitObjects = 1; - - return GetScore(mode, accuracyRatio, comboRatio, statistics, totalHitObjects); + return GetScore(mode, accuracyRatio, comboRatio, maxBasicHitObjects, statistics); } - public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, Dictionary statistics, int totalHitObjects) + /// + /// Computes the total score from given scoring component ratios. + /// + /// + /// Does not require an to have been applied via before use. + /// + /// The to represent the score as. + /// The accuracy percentage achieved by the player. + /// The portion of the max combo achieved by the player. + /// Any additional statistics to be factored in. + /// The total number of basic (non-tick and non-bonus) hitobjects in the beatmap. + /// The total score computed from the given scoring component ratios. + public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, int totalBasicHitObjects, Dictionary statistics) { switch (mode) { @@ -268,7 +310,7 @@ namespace osu.Game.Rulesets.Scoring // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score; - return Math.Pow(scaledStandardised * totalHitObjects, 2) * 36; + return Math.Pow(scaledStandardised * totalBasicHitObjects, 2) * 36; } } @@ -326,10 +368,12 @@ namespace osu.Game.Rulesets.Scoring { maxAchievableCombo = HighestCombo.Value; maxBaseScore = baseScore; + maxBasicHitObjects = basicHitObjects; } baseScore = 0; rollingMaxBaseScore = 0; + basicHitObjects = 0; TotalScore.Value = 0; Accuracy.Value = 1; @@ -355,11 +399,6 @@ namespace osu.Game.Rulesets.Scoring score.HitEvents = hitEvents; } - /// - /// Maximum for a normal hit (i.e. not tick/bonus) for this ruleset. Only populated via . - /// - private HitResult? maxNormalResult; - public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) { base.ResetFromReplayFrame(ruleset, frame); @@ -394,7 +433,7 @@ namespace osu.Game.Rulesets.Scoring break; default: - maxResult = maxNormalResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; + maxResult = maxBasicHitResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; break; } From f1c40bd9eda6dfda1bcd6669c548b8e078c8396f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 12:57:59 +0900 Subject: [PATCH 5/7] Rework GetScore() method signatures + implementations Rename legacy-facing overload to mention as much --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 25 ++- .../Visual/Gameplay/TestSceneFailJudgement.cs | 2 +- .../TestSceneMultiSpectatorLeaderboard.cs | 5 +- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- ...ceneMultiplayerGameplayLeaderboardTeams.cs | 2 +- .../PerformanceBreakdownCalculator.cs | 3 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 190 ++++++++++-------- osu.Game/Scoring/ScoreManager.cs | 15 +- .../Multiplayer/MultiplayerPlayer.cs | 2 +- .../Spectate/MultiSpectatorLeaderboard.cs | 11 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../HUD/MultiplayerGameplayLeaderboard.cs | 25 ++- 13 files changed, 173 insertions(+), 113 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index e96ff1f7f1..d327f4844e 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -6,11 +6,15 @@ using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Rulesets.Scoring @@ -300,7 +304,26 @@ namespace osu.Game.Tests.Rulesets.Scoring HitObjects = { new TestHitObject(result) } }); - Assert.That(scoreProcessor.GetScore(ScoringMode.Standardised, result.AffectsCombo() ? 1 : 0, statistic), Is.EqualTo(expectedScore).Within(0.5d)); + Assert.That(scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, new ScoreInfo + { + Ruleset = new TestRuleset().RulesetInfo, + MaxCombo = result.AffectsCombo() ? 1 : 0, + Statistics = statistic + }), Is.EqualTo(expectedScore).Within(0.5d)); + } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => throw new System.NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException(); + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException(); + + public override string Description { get; } + public override string ShortName { get; } } private class TestJudgement : Judgement diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 6430c29dfa..79d7bb366d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => { - var score = new ScoreInfo(); + var score = new ScoreInfo { Ruleset = Ruleset.Value }; ((FailPlayer)Player).ScoreProcessor.PopulateScore(score); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 42bb99de24..f57a54d84c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -46,7 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer var scoreProcessor = new OsuScoreProcessor(); scoreProcessor.ApplyBeatmap(playable); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) + { + Expanded = { Value = true } + }, Add); }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index cfac5da4ff..bcd4474876 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Multiplayer scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index f751b162d1..7f5aced925 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index dfb8066b28..fc38ed0298 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Difficulty // calculate total score ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); - scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo; scoreProcessor.Mods.Value = perfectPlay.Mods; - perfectPlay.TotalScore = (long)scoreProcessor.GetScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); + perfectPlay.TotalScore = (long)scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, perfectPlay); // compute rank achieved // default to SS, then adjust the rank with mods diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b0656e270e..3004464df7 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Utils; -using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; @@ -93,10 +92,10 @@ namespace osu.Game.Rulesets.Scoring private int maxBasicHitObjects; /// - /// Maximum for a normal hit (i.e. not tick/bonus) for this ruleset. - /// Only populated via . + /// The maximum of a basic (non-tick and non-bonus) hitobject. + /// Only populated via or . /// - private HitResult? maxBasicHitResult; + private HitResult? maxBasicResult; private double rollingMaxBaseScore; private double baseScore; @@ -222,81 +221,110 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { - if (rollingMaxBaseScore != 0) - Accuracy.Value = calculateAccuracyRatio(baseScore, true); + double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1; + double accuracyRatio = maxBaseScore > 0 ? baseScore / maxBaseScore : 1; + double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1; - TotalScore.Value = GetScore(Mode.Value); + Accuracy.Value = rollingAccuracyRatio; + TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maxBasicHitObjects); } /// - /// Computes the total score from judgements that have been applied to this - /// through and . + /// Computes the total score of a given finalised . This should be used when a score is known to be complete. /// /// - /// Requires an to have been applied via before use. + /// Does not require to have been called before use. /// /// The to represent the score as. + /// The to compute the total score of. /// The total score in the given . - public double GetScore(ScoringMode mode) + public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo) { - return GetScore(mode, calculateAccuracyRatio(baseScore), calculateComboRatio(HighestCombo.Value), scoreResultCounts); + extractFromStatistics(scoreInfo.Ruleset.CreateInstance(), + scoreInfo.Statistics, + out double extractedBaseScore, + out double extractedMaxBaseScore, + out int extractedMaxCombo, + out int extractedBasicHitObjects); + + double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1; + double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1; + + return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects); } /// - /// Computes the total score from judgements counts in a statistics dictionary. + /// Computes the total score of a partially-completed . This should be used when it is unknown whether a score is complete. /// /// - /// Requires an to have been applied via before use. + /// Requires to have been called before use. /// /// The to represent the score as. - /// The maximum combo achievable in the beatmap. - /// The statistics to compute the score for. - /// The total score computed from judgements in the statistics dictionary. - public double GetScore(ScoringMode mode, int maxCombo, Dictionary statistics) + /// The to compute the total score of. + /// The total score in the given . + public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo) { - // calculate base score from statistics pairs - int computedBaseScore = 0; + extractFromStatistics(scoreInfo.Ruleset.CreateInstance(), + scoreInfo.Statistics, + out double extractedBaseScore, + out _, + out _, + out _); - foreach (var pair in statistics) + double accuracyRatio = maxBaseScore > 0 ? extractedBaseScore / maxBaseScore : 1; + double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; + + return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maxBasicHitObjects); + } + + /// + /// Computes the total score of a given with a given custom max achievable combo. + /// + /// + /// This is useful for processing legacy scores in which the maximum achievable combo can be more accurately determined via external means (e.g. database values or difficulty calculation). + ///

Does not require to have been called before use.

+ ///
+ /// The to represent the score as. + /// The to compute the total score of. + /// The maximum achievable combo for the provided beatmap. + /// The total score in the given . + public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo) + { + double accuracyRatio = scoreInfo.Accuracy; + double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; + + // For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score. + // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. + // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. + if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3) { - if (!pair.Key.AffectsAccuracy()) - continue; + extractFromStatistics( + scoreInfo.Ruleset.CreateInstance(), + scoreInfo.Statistics, + out double computedBaseScore, + out double computedMaxBaseScore, + out _, + out _); - computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; + if (computedMaxBaseScore > 0) + accuracyRatio = computedBaseScore / computedMaxBaseScore; } - return GetScore(mode, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), statistics); + int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum(); + + return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects); } /// - /// Computes the total score from given scoring component ratios. + /// Computes the total score from individual scoring components. /// - /// - /// Requires an to have been applied via before use. - /// /// The to represent the score as. /// The accuracy percentage achieved by the player. /// The portion of the max combo achieved by the player. - /// Any additional statistics to be factored in. - /// The total score computed from the given scoring component ratios. - public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, Dictionary statistics) - { - return GetScore(mode, accuracyRatio, comboRatio, maxBasicHitObjects, statistics); - } - - /// - /// Computes the total score from given scoring component ratios. - /// - /// - /// Does not require an to have been applied via before use. - /// - /// The to represent the score as. - /// The accuracy percentage achieved by the player. - /// The portion of the max combo achieved by the player. - /// Any additional statistics to be factored in. + /// The total bonus score. /// The total number of basic (non-tick and non-bonus) hitobjects in the beatmap. /// The total score computed from the given scoring component ratios. - public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, int totalBasicHitObjects, Dictionary statistics) + public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects) { switch (mode) { @@ -304,33 +332,22 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; + return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. - double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score; - return Math.Pow(scaledStandardised * totalBasicHitObjects, 2) * 36; + double scaledStandardised = ComputeScore(ScoringMode.Standardised, accuracyRatio, comboRatio, bonusScore, totalBasicHitObjects) / max_score; + return Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * 36; } } /// - /// Get the accuracy fraction for the provided base score. + /// Calculates the total bonus score from score statistics. /// - /// The score to be used for accuracy calculation. - /// Whether the rolling base score should be used (ie. for the current point in time based on Apply/Reverted results). - /// The computed accuracy. - private double calculateAccuracyRatio(double baseScore, bool preferRolling = false) - { - if (preferRolling && rollingMaxBaseScore != 0) - return baseScore / rollingMaxBaseScore; - - return maxBaseScore > 0 ? baseScore / maxBaseScore : 1; - } - - private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; - - private double getBonusScore(Dictionary statistics) + /// The score statistics. + /// The total bonus score. + private double getBonusScore(IReadOnlyDictionary statistics) => statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE + statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; @@ -387,16 +404,17 @@ namespace osu.Game.Rulesets.Scoring /// public virtual void PopulateScore(ScoreInfo score) { - score.TotalScore = (long)Math.Round(GetScore(ScoringMode.Standardised)); score.Combo = Combo.Value; score.MaxCombo = HighestCombo.Value; score.Accuracy = Accuracy.Value; score.Rank = Rank.Value; + score.HitEvents = hitEvents; foreach (var result in HitResultExtensions.ALL_TYPES) score.Statistics[result] = GetStatistic(result); - score.HitEvents = hitEvents; + // Populate total score after everything else. + score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score)); } public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) @@ -406,11 +424,26 @@ namespace osu.Game.Rulesets.Scoring if (frame.Header == null) return; - baseScore = 0; - rollingMaxBaseScore = 0; + extractFromStatistics(ruleset, frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _); HighestCombo.Value = frame.Header.MaxCombo; - foreach ((HitResult result, int count) in frame.Header.Statistics) + scoreResultCounts.Clear(); + scoreResultCounts.AddRange(frame.Header.Statistics); + + updateScore(); + + OnResetFromReplayFrame?.Invoke(); + } + + private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary statistics, out double baseScore, out double maxBaseScore, out int maxCombo, + out int basicHitObjects) + { + baseScore = 0; + maxBaseScore = 0; + maxCombo = 0; + basicHitObjects = 0; + + foreach ((HitResult result, int count) in statistics) { // Bonus scores are counted separately directly from the statistics dictionary later on. if (!result.IsScorable() || result.IsBonus()) @@ -433,20 +466,19 @@ namespace osu.Game.Rulesets.Scoring break; default: - maxResult = maxBasicHitResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; + maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; break; } baseScore += count * Judgement.ToNumericResult(result); - rollingMaxBaseScore += count * Judgement.ToNumericResult(maxResult); + maxBaseScore += count * Judgement.ToNumericResult(maxResult); + + if (result.AffectsCombo()) + maxCombo += count; + + if (result.IsBasic()) + basicHitObjects += count; } - - scoreResultCounts.Clear(); - scoreResultCounts.AddRange(frame.Header.Statistics); - - updateScore(); - - OnResetFromReplayFrame?.Invoke(); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 963c4a77ca..6e49e3c9af 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -18,7 +18,6 @@ using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring @@ -136,21 +135,9 @@ namespace osu.Game.Scoring return score.TotalScore; int beatmapMaxCombo; - double accuracy = score.Accuracy; if (score.IsLegacyScore) { - if (score.RulesetID == 3) - { - // In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score. - // To get around this, recalculate accuracy based on the hit statistics. - // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. - double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect); - double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum(); - if (maxBaseScore > 0) - accuracy = baseScore / maxBaseScore; - } - // This score is guaranteed to be an osu!stable score. // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. if (score.BeatmapInfo.MaxCombo != null) @@ -184,7 +171,7 @@ namespace osu.Game.Scoring var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - return (long)Math.Round(scoreProcessor.GetScore(mode, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics)); + return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo)); } /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index bd2f49a9e5..d8fb448437 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }); // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, users), l => + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(GameplayState.Ruleset.RulesetInfo, ScoreProcessor, users), l => { if (!LoadedBeatmapSuccessfully) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index 1614828a78..4545913db8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs @@ -5,6 +5,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Timing; using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -12,8 +13,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) - : base(scoreProcessor, users) + public MultiSpectatorLeaderboard(RulesetInfo ruleset, [NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) + : base(ruleset, scoreProcessor, users) { } @@ -33,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ((SpectatingTrackedUserData)data).Clock = null; } - protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, scoreProcessor); + protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, ruleset, scoreProcessor); protected override void Update() { @@ -48,8 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [CanBeNull] public IClock Clock; - public SpectatingTrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) - : base(user, scoreProcessor) + public SpectatingTrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) + : base(user, ruleset, scoreProcessor) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 3bb76c4a76..6747b8fc66 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users) + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, users) { Expanded = { Value = true }, }, l => diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 77eaf133c7..5a7762a3d8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetScore(ScoringMode.Standardised)); + Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeFinalScore(ScoringMode.Standardised, Score.ScoreInfo)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 0516e00b8b..71998622ef 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -17,7 +17,9 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; +using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD @@ -41,6 +43,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private UserLookupCache userLookupCache { get; set; } + private readonly RulesetInfo ruleset; private readonly ScoreProcessor scoreProcessor; private readonly MultiplayerRoomUser[] playingUsers; private Bindable scoringMode; @@ -52,11 +55,13 @@ namespace osu.Game.Screens.Play.HUD /// /// Construct a new leaderboard. /// + /// The ruleset. /// A score processor instance to handle score calculation for scores of users in the match. /// IDs of all users in this match. - public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) + public MultiplayerGameplayLeaderboard(RulesetInfo ruleset, ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) { // todo: this will eventually need to be created per user to support different mod combinations. + this.ruleset = ruleset; this.scoreProcessor = scoreProcessor; playingUsers = users; @@ -69,7 +74,7 @@ namespace osu.Game.Screens.Play.HUD foreach (var user in playingUsers) { - var trackedUser = CreateUserData(user, scoreProcessor); + var trackedUser = CreateUserData(user, ruleset, scoreProcessor); trackedUser.ScoringMode.BindTo(scoringMode); UserScores[user.UserID] = trackedUser; @@ -119,7 +124,7 @@ namespace osu.Game.Screens.Play.HUD spectatorClient.OnNewFrames += handleIncomingFrames; } - protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new TrackedUserData(user, scoreProcessor); + protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new TrackedUserData(user, ruleset, scoreProcessor); protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked) { @@ -222,8 +227,12 @@ namespace osu.Game.Screens.Play.HUD public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID; - public TrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) + private readonly RulesetInfo ruleset; + + public TrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) { + this.ruleset = ruleset; + User = user; ScoreProcessor = scoreProcessor; @@ -244,7 +253,13 @@ namespace osu.Game.Screens.Play.HUD { var header = frame.Header; - Score.Value = ScoreProcessor.GetScore(ScoringMode.Value, header.MaxCombo, header.Statistics); + Score.Value = ScoreProcessor.ComputePartialScore(ScoringMode.Value, new ScoreInfo + { + Ruleset = ruleset, + MaxCombo = header.MaxCombo, + Statistics = header.Statistics + }); + Accuracy.Value = header.Accuracy; CurrentCombo.Value = header.Combo; } From 6fd8b4d8918f9095c9651cf88047c5467c10b06e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 22:14:58 +0900 Subject: [PATCH 6/7] Safeguard method against invalid invocation --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 3004464df7..5e11d4da72 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; @@ -100,6 +101,7 @@ namespace osu.Game.Rulesets.Scoring private double rollingMaxBaseScore; private double baseScore; private int basicHitObjects; + private bool beatmapApplied; private readonly Dictionary scoreResultCounts = new Dictionary(); private readonly List hitEvents = new List(); @@ -135,6 +137,12 @@ namespace osu.Game.Rulesets.Scoring }; } + public override void ApplyBeatmap(IBeatmap beatmap) + { + base.ApplyBeatmap(beatmap); + beatmapApplied = true; + } + protected sealed override void ApplyResultInternal(JudgementResult result) { result.ComboAtJudgement = Combo.Value; @@ -264,6 +272,9 @@ namespace osu.Game.Rulesets.Scoring /// The total score in the given . public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo) { + if (!beatmapApplied) + throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); + extractFromStatistics(scoreInfo.Ruleset.CreateInstance(), scoreInfo.Statistics, out double extractedBaseScore, From e91226f5786e1d481c7bffcbe778a77056e6eed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 8 Mar 2022 21:41:10 +0100 Subject: [PATCH 7/7] Fix 'auto-property never assigned to' inspections --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index d327f4844e..2a19d51c9d 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -322,8 +322,8 @@ namespace osu.Game.Tests.Rulesets.Scoring public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException(); - public override string Description { get; } - public override string ShortName { get; } + public override string Description => string.Empty; + public override string ShortName => string.Empty; } private class TestJudgement : Judgement