1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:37:28 +08:00

Merge pull request #17164 from smoogipoo/scoreprocessor-rework

Rework ScoreProcessor score calculation methods to fix various issues
This commit is contained in:
Dean Herbert 2022-03-10 16:48:58 +09:00 committed by GitHub
commit db5c2c15dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 228 additions and 120 deletions

View File

@ -6,11 +6,15 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Rulesets.Scoring namespace osu.Game.Tests.Rulesets.Scoring
@ -300,7 +304,26 @@ namespace osu.Game.Tests.Rulesets.Scoring
HitObjects = { new TestHitObject(result) } HitObjects = { new TestHitObject(result) }
}); });
Assert.That(scoreProcessor.GetImmediateScore(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<Mod> GetModsFor(ModType type) => throw new System.NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> 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 => string.Empty;
public override string ShortName => string.Empty;
} }
private class TestJudgement : Judgement private class TestJudgement : Judgement

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total number of results == 1", () => AddAssert("total number of results == 1", () =>
{ {
var score = new ScoreInfo(); var score = new ScoreInfo { Ruleset = Ruleset.Value };
((FailPlayer)Player).ScoreProcessor.PopulateScore(score); ((FailPlayer)Player).ScoreProcessor.PopulateScore(score);

View File

@ -46,7 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor(); var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable); 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); AddUntilStep("wait for load", () => leaderboard.IsLoaded);

View File

@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playableBeatmap); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playableBeatmap); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Difficulty
// calculate total score // calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo;
scoreProcessor.Mods.Value = perfectPlay.Mods; scoreProcessor.Mods.Value = perfectPlay.Mods;
perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); perfectPlay.TotalScore = (long)scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, perfectPlay);
// compute rank achieved // compute rank achieved
// default to SS, then adjust the rank with mods // default to SS, then adjust the rank with mods

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -91,9 +92,23 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
private double maxBaseScore; private double maxBaseScore;
/// <summary>
/// The maximum number of basic (non-tick and non-bonus) hitobjects.
/// </summary>
private int maxBasicHitObjects;
/// <summary>
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
/// Only populated via <see cref="ComputeFinalScore"/> or <see cref="ResetFromReplayFrame"/>.
/// </summary>
private HitResult? maxBasicResult;
private double rollingMaxBaseScore; private double rollingMaxBaseScore;
private double baseScore; private double baseScore;
private int basicHitObjects;
private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
private readonly List<HitEvent> hitEvents = new List<HitEvent>(); private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject lastHitObject; private HitObject lastHitObject;
@ -127,7 +142,11 @@ namespace osu.Game.Rulesets.Scoring
}; };
} }
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>(); public override void ApplyBeatmap(IBeatmap beatmap)
{
base.ApplyBeatmap(beatmap);
beatmapApplied = true;
}
protected sealed override void ApplyResultInternal(JudgementResult result) protected sealed override void ApplyResultInternal(JudgementResult result)
{ {
@ -165,6 +184,9 @@ namespace osu.Game.Rulesets.Scoring
rollingMaxBaseScore += result.Judgement.MaxNumericResult; rollingMaxBaseScore += result.Judgement.MaxNumericResult;
} }
if (result.Type.IsBasic())
basicHitObjects++;
hitEvents.Add(CreateHitEvent(result)); hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject; lastHitObject = result.HitObject;
@ -200,6 +222,9 @@ namespace osu.Game.Rulesets.Scoring
rollingMaxBaseScore -= result.Judgement.MaxNumericResult; rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
} }
if (result.Type.IsBasic())
basicHitObjects--;
Debug.Assert(hitEvents.Count > 0); Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject; lastHitObject = hitEvents[^1].LastHitObject;
hitEvents.RemoveAt(hitEvents.Count - 1); hitEvents.RemoveAt(hitEvents.Count - 1);
@ -209,29 +234,113 @@ namespace osu.Game.Rulesets.Scoring
private void updateScore() private void updateScore()
{ {
if (rollingMaxBaseScore != 0) double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1;
Accuracy.Value = calculateAccuracyRatio(baseScore, true); 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);
private double getScore(ScoringMode mode)
{
return GetScore(mode,
calculateAccuracyRatio(baseScore),
calculateComboRatio(HighestCombo.Value),
scoreResultCounts);
} }
/// <summary> /// <summary>
/// Computes the total score. /// Computes the total score of a given finalised <see cref="ScoreInfo"/>. This should be used when a score is known to be complete.
/// </summary> /// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param> /// <remarks>
/// Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
/// </remarks>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
{
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);
}
/// <summary>
/// Computes the total score of a partially-completed <see cref="ScoreInfo"/>. This should be used when it is unknown whether a score is complete.
/// </summary>
/// <remarks>
/// Requires <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
/// </remarks>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
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,
out _,
out _,
out _);
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);
}
/// <summary>
/// Computes the total score of a given <see cref="ScoreInfo"/> with a given custom max achievable combo.
/// </summary>
/// <remarks>
/// 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).
/// <p>Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.</p>
/// </remarks>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <param name="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
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)
{
extractFromStatistics(
scoreInfo.Ruleset.CreateInstance(),
scoreInfo.Statistics,
out double computedBaseScore,
out double computedMaxBaseScore,
out _,
out _);
if (computedMaxBaseScore > 0)
accuracyRatio = computedBaseScore / computedMaxBaseScore;
}
int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum();
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects);
}
/// <summary>
/// Computes the total score from individual scoring components.
/// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="accuracyRatio">The accuracy percentage achieved by the player.</param> /// <param name="accuracyRatio">The accuracy percentage achieved by the player.</param>
/// <param name="comboRatio">The proportion of the max combo achieved by the player.</param> /// <param name="comboRatio">The portion of the max combo achieved by the player.</param>
/// <param name="statistics">Any statistics to be factored in.</param> /// <param name="bonusScore">The total bonus score.</param>
/// <returns>The total score.</returns> /// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, Dictionary<HitResult, int> statistics) /// <returns>The total score computed from the given scoring component ratios.</returns>
public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects)
{ {
switch (mode) switch (mode)
{ {
@ -239,62 +348,22 @@ namespace osu.Game.Rulesets.Scoring
case ScoringMode.Standardised: case ScoringMode.Standardised:
double accuracyScore = accuracyPortion * accuracyRatio; double accuracyScore = accuracyPortion * accuracyRatio;
double comboScore = comboPortion * comboRatio; double comboScore = comboPortion * comboRatio;
return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
case ScoringMode.Classic: 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. // 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. // 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; double scaledStandardised = ComputeScore(ScoringMode.Standardised, accuracyRatio, comboRatio, bonusScore, totalBasicHitObjects) / max_score;
return Math.Pow(scaledStandardised * totalHitObjects, 2) * ClassicScoreMultiplier; return Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier;
} }
} }
/// <summary> /// <summary>
/// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time. /// Calculates the total bonus score from score statistics.
/// </summary> /// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param> /// <param name="statistics">The score statistics.</param>
/// <param name="maxCombo">The maximum combo achievable in the beatmap.</param> /// <returns>The total bonus score.</returns>
/// <param name="statistics">Statistics to be used for calculating accuracy, bonus score, etc.</param> private double getBonusScore(IReadOnlyDictionary<HitResult, int> statistics)
/// <returns>The computed score for provided inputs.</returns>
public double GetImmediateScore(ScoringMode mode, int maxCombo, Dictionary<HitResult, int> 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);
}
/// <summary>
/// Get the accuracy fraction for the provided base score.
/// </summary>
/// <param name="baseScore">The score to be used for accuracy calculation.</param>
/// <param name="preferRolling">Whether the rolling base score should be used (ie. for the current point in time based on Apply/Reverted results).</param>
/// <returns>The computed accuracy.</returns>
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<HitResult, int> statistics)
=> statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE => statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
+ statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; + statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
@ -316,8 +385,6 @@ namespace osu.Game.Rulesets.Scoring
public int GetStatistic(HitResult result) => scoreResultCounts.GetValueOrDefault(result); public int GetStatistic(HitResult result) => scoreResultCounts.GetValueOrDefault(result);
public double GetStandardisedScore() => getScore(ScoringMode.Standardised);
/// <summary> /// <summary>
/// Resets this ScoreProcessor to a default state. /// Resets this ScoreProcessor to a default state.
/// </summary> /// </summary>
@ -334,10 +401,12 @@ namespace osu.Game.Rulesets.Scoring
{ {
maxAchievableCombo = HighestCombo.Value; maxAchievableCombo = HighestCombo.Value;
maxBaseScore = baseScore; maxBaseScore = baseScore;
maxBasicHitObjects = basicHitObjects;
} }
baseScore = 0; baseScore = 0;
rollingMaxBaseScore = 0; rollingMaxBaseScore = 0;
basicHitObjects = 0;
TotalScore.Value = 0; TotalScore.Value = 0;
Accuracy.Value = 1; Accuracy.Value = 1;
@ -351,23 +420,19 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
public virtual void PopulateScore(ScoreInfo score) public virtual void PopulateScore(ScoreInfo score)
{ {
score.TotalScore = (long)Math.Round(GetStandardisedScore());
score.Combo = Combo.Value; score.Combo = Combo.Value;
score.MaxCombo = HighestCombo.Value; score.MaxCombo = HighestCombo.Value;
score.Accuracy = Accuracy.Value; score.Accuracy = Accuracy.Value;
score.Rank = Rank.Value; score.Rank = Rank.Value;
score.HitEvents = hitEvents;
foreach (var result in HitResultExtensions.ALL_TYPES) foreach (var result in HitResultExtensions.ALL_TYPES)
score.Statistics[result] = GetStatistic(result); score.Statistics[result] = GetStatistic(result);
score.HitEvents = hitEvents; // Populate total score after everything else.
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
} }
/// <summary>
/// Maximum <see cref="HitResult"/> for a normal hit (i.e. not tick/bonus) for this ruleset. Only populated via <see cref="ResetFromReplayFrame"/>.
/// </summary>
private HitResult? maxNormalResult;
public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
{ {
base.ResetFromReplayFrame(ruleset, frame); base.ResetFromReplayFrame(ruleset, frame);
@ -375,11 +440,26 @@ namespace osu.Game.Rulesets.Scoring
if (frame.Header == null) if (frame.Header == null)
return; return;
baseScore = 0; extractFromStatistics(ruleset, frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _);
rollingMaxBaseScore = 0;
HighestCombo.Value = frame.Header.MaxCombo; 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<HitResult, int> 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. // Bonus scores are counted separately directly from the statistics dictionary later on.
if (!result.IsScorable() || result.IsBonus()) if (!result.IsScorable() || result.IsBonus())
@ -402,20 +482,19 @@ namespace osu.Game.Rulesets.Scoring
break; break;
default: default:
maxResult = maxNormalResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
break; break;
} }
baseScore += count * Judgement.ToNumericResult(result); 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) protected override void Dispose(bool isDisposing)

View File

@ -18,7 +18,6 @@ using osu.Game.Database;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring namespace osu.Game.Scoring
@ -136,21 +135,9 @@ namespace osu.Game.Scoring
return score.TotalScore; return score.TotalScore;
int beatmapMaxCombo; int beatmapMaxCombo;
double accuracy = score.Accuracy;
if (score.IsLegacyScore) 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. // 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. // 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) if (score.BeatmapInfo.MaxCombo != null)
@ -184,7 +171,7 @@ namespace osu.Game.Scoring
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods; 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));
} }
/// <summary> /// <summary>

View File

@ -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. // 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) if (!LoadedBeatmapSuccessfully)
return; return;

View File

@ -5,6 +5,7 @@ using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -12,8 +13,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
{ {
public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) public MultiSpectatorLeaderboard(RulesetInfo ruleset, [NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
: base(scoreProcessor, users) : base(ruleset, scoreProcessor, users)
{ {
} }
@ -33,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
((SpectatingTrackedUserData)data).Clock = null; ((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() protected override void Update()
{ {
@ -48,8 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[CanBeNull] [CanBeNull]
public IClock Clock; public IClock Clock;
public SpectatingTrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) public SpectatingTrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor)
: base(user, scoreProcessor) : base(user, ruleset, scoreProcessor)
{ {
} }

View File

@ -133,7 +133,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
scoreProcessor.ApplyBeatmap(playableBeatmap); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users) LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, users)
{ {
Expanded = { Value = true }, Expanded = { Value = true },
}, l => }, l =>

View File

@ -11,6 +11,7 @@ using osu.Framework.Screens;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -64,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeFinalScore(ScoringMode.Standardised, Score.ScoreInfo));
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -17,7 +17,9 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
@ -41,6 +43,7 @@ namespace osu.Game.Screens.Play.HUD
[Resolved] [Resolved]
private UserLookupCache userLookupCache { get; set; } private UserLookupCache userLookupCache { get; set; }
private readonly RulesetInfo ruleset;
private readonly ScoreProcessor scoreProcessor; private readonly ScoreProcessor scoreProcessor;
private readonly MultiplayerRoomUser[] playingUsers; private readonly MultiplayerRoomUser[] playingUsers;
private Bindable<ScoringMode> scoringMode; private Bindable<ScoringMode> scoringMode;
@ -52,11 +55,13 @@ namespace osu.Game.Screens.Play.HUD
/// <summary> /// <summary>
/// Construct a new leaderboard. /// Construct a new leaderboard.
/// </summary> /// </summary>
/// <param name="ruleset">The ruleset.</param>
/// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param> /// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param>
/// <param name="users">IDs of all users in this match.</param> /// <param name="users">IDs of all users in this match.</param>
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. // todo: this will eventually need to be created per user to support different mod combinations.
this.ruleset = ruleset;
this.scoreProcessor = scoreProcessor; this.scoreProcessor = scoreProcessor;
playingUsers = users; playingUsers = users;
@ -69,7 +74,7 @@ namespace osu.Game.Screens.Play.HUD
foreach (var user in playingUsers) foreach (var user in playingUsers)
{ {
var trackedUser = CreateUserData(user, scoreProcessor); var trackedUser = CreateUserData(user, ruleset, scoreProcessor);
trackedUser.ScoringMode.BindTo(scoringMode); trackedUser.ScoringMode.BindTo(scoringMode);
UserScores[user.UserID] = trackedUser; UserScores[user.UserID] = trackedUser;
@ -119,7 +124,7 @@ namespace osu.Game.Screens.Play.HUD
spectatorClient.OnNewFrames += handleIncomingFrames; 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) 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 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; User = user;
ScoreProcessor = scoreProcessor; ScoreProcessor = scoreProcessor;
@ -244,7 +253,13 @@ namespace osu.Game.Screens.Play.HUD
{ {
var header = frame.Header; var header = frame.Header;
Score.Value = ScoreProcessor.GetImmediateScore(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; Accuracy.Value = header.Accuracy;
CurrentCombo.Value = header.Combo; CurrentCombo.Value = header.Combo;
} }