From 5afe57033ddc19da134009b4967409022b2f8dea Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 9 May 2023 19:30:54 +0900 Subject: [PATCH 01/41] Add parent hitobject to strong hits --- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 7 ++++++- osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs | 6 +++++- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 6 +++++- osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs | 7 +++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index b4a12fd314b6..80553a1033f1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -109,12 +109,17 @@ private void createTicks(CancellationToken cancellationToken) protected override HitWindows CreateHitWindows() => HitWindows.Empty; - protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; + protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this) { StartTime = startTime }; public class StrongNestedHit : StrongNestedHitObject { // The strong hit of the drum roll doesn't actually provide any score. public override Judgement CreateJudgement() => new IgnoreJudgement(); + + public StrongNestedHit(TaikoHitObject parent) + : base(parent) + { + } } #region LegacyBeatmapEncoder diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 6bcb8674e6a7..56dbe3ce3806 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -33,10 +33,14 @@ public class DrumRollTick : TaikoStrongableHitObject public override double MaximumJudgementOffset => HitWindow; - protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; + protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this) { StartTime = startTime }; public class StrongNestedHit : StrongNestedHitObject { + public StrongNestedHit(TaikoHitObject parent) + : base(parent) + { + } } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 787079bfeea8..6a3c8467e970 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -72,10 +72,14 @@ private void updateSamplesFromType() } } - protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; + protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this) { StartTime = startTime }; public class StrongNestedHit : StrongNestedHitObject { + public StrongNestedHit(TaikoHitObject parent) + : base(parent) + { + } } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs index 628c41d87820..316115f44dbd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs @@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public abstract class StrongNestedHitObject : TaikoHitObject { + public readonly TaikoHitObject Parent; + + protected StrongNestedHitObject(TaikoHitObject parent) + { + Parent = parent; + } + public override Judgement CreateJudgement() => new TaikoStrongJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; From 3c3c812ed6dfca27d7ffa2db07703c93d57d52b9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 9 May 2023 19:33:33 +0900 Subject: [PATCH 02/41] Initial implementation of ScoreV2 --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Scoring/CatchScoreProcessor.cs | 78 +++- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Scoring/ManiaScoreProcessor.cs | 49 ++- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Scoring/OsuScoreProcessor.cs | 37 +- .../Scoring/TaikoScoreProcessor.cs | 58 ++- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- .../Spectator/SpectatorScoreProcessor.cs | 3 +- .../PerformanceBreakdownCalculator.cs | 4 +- .../Rulesets/Mods/ModAccuracyChallenge.cs | 4 +- osu.Game/Rulesets/Ruleset.cs | 21 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 384 ++++-------------- osu.Game/Scoring/ScoreManager.cs | 4 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 3 +- 15 files changed, 296 insertions(+), 357 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 8a0b8250d561..e2255fa6f73b 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -34,7 +34,7 @@ public class CatchRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableCatchRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index b6a42407da7b..bf5c3a91d644 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -1,17 +1,87 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public partial class CatchScoreProcessor : ScoreProcessor { - public CatchScoreProcessor() - : base(new CatchRuleset()) + private const int combo_cap = 200; + private const double combo_base = 4; + + protected override double ClassicScoreMultiplier => 28; + + private double tinyDropletScale; + + private int maximumTinyDroplets; + private int hitTinyDroplets; + + public CatchScoreProcessor(Ruleset ruleset) + : base(ruleset) { } - protected override double ClassicScoreMultiplier => 28; + protected override double ComputeTotalScore() + { + double fruitHitsRatio = maximumTinyDroplets == 0 ? 0 : (double)hitTinyDroplets / maximumTinyDroplets; + + const int tiny_droplets_portion = 400000; + + return + (int)Math.Round + (( + ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * ComboPortion / MaxComboPortion + + tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + + BonusPortion + ) * ScoreMultiplier); + } + + protected override void AddScoreChange(JudgementResult result) + { + var change = computeScoreChange(result); + ComboPortion += change.combo; + BonusPortion += change.bonus; + hitTinyDroplets += change.tinyDropletHits; + } + + protected override void RemoveScoreChange(JudgementResult result) + { + var change = computeScoreChange(result); + ComboPortion -= change.combo; + BonusPortion -= change.bonus; + hitTinyDroplets -= change.tinyDropletHits; + } + + private (double combo, double bonus, int tinyDropletHits) computeScoreChange(JudgementResult result) + { + if (result.HitObject is TinyDroplet) + return (0, 0, 1); + + if (result.Type.IsBonus()) + return (0, Judgement.ToNumericResult(result.Type), 0); + + return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(combo_cap, combo_base)), 0, 0); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + if (storeResults) + { + maximumTinyDroplets = hitTinyDroplets; + + if (maximumTinyDroplets + MaxBasicJudgements == 0) + tinyDropletScale = 0; + else + tinyDropletScale = (double)maximumTinyDroplets / (maximumTinyDroplets + MaxBasicJudgements); + } + + hitTinyDroplets = 0; + } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index d324682989f3..c6065c9b9615 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -50,7 +50,7 @@ public class ManiaRuleset : Ruleset, ILegacyRuleset public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableManiaRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime); diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index f724972a2977..7d188c678646 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -1,23 +1,54 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { - internal partial class ManiaScoreProcessor : ScoreProcessor + public partial class ManiaScoreProcessor : ScoreProcessor { - public ManiaScoreProcessor() - : base(new ManiaRuleset()) + private const double combo_base = 4; + + protected override double ClassicScoreMultiplier => 16; + + public ManiaScoreProcessor(Ruleset ruleset) + : base(ruleset) { } - protected override double DefaultAccuracyPortion => 0.99; + protected override double ComputeTotalScore() + { + return + (int)Math.Round + (( + 200000 * ComboPortion / MaxComboPortion + + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier); + } - protected override double DefaultComboPortion => 0.01; + protected override void AddScoreChange(JudgementResult result) + { + var change = computeScoreChange(result); + ComboPortion += change.combo; + BonusPortion += change.bonus; + } - protected override double ClassicScoreMultiplier => 16; + protected override void RemoveScoreChange(JudgementResult result) + { + var change = computeScoreChange(result); + ComboPortion -= change.combo; + BonusPortion -= change.bonus; + } + + private (double combo, double bonus) computeScoreChange(JudgementResult result) + { + if (result.Type.IsBonus()) + return (0, Judgement.ToNumericResult(result.Type)); + + return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0); + } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 922594a93ae2..497d40543657 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -45,7 +45,7 @@ public class OsuRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableOsuRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 50d4eb62588e..5f5997b0c1b3 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,38 +1,29 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Osu.Objects; +using System; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { public partial class OsuScoreProcessor : ScoreProcessor { - public OsuScoreProcessor() - : base(new OsuRuleset()) - { - } - protected override double ClassicScoreMultiplier => 36; - protected override HitEvent CreateHitEvent(JudgementResult result) - => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); - - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) + public OsuScoreProcessor(Ruleset ruleset) + : base(ruleset) { - switch (hitObject) - { - case HitCircle: - return new OsuHitCircleJudgementResult(hitObject, judgement); + } - default: - return new OsuJudgementResult(hitObject, judgement); - } + protected override double ComputeTotalScore() + { + return + (int)Math.Round + (( + 700000 * ComboPortion / MaxComboPortion + + 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier); } } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 4b60ee3ccb42..caf0a799a283 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -1,23 +1,63 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Scoring { - internal partial class TaikoScoreProcessor : ScoreProcessor + public partial class TaikoScoreProcessor : ScoreProcessor { - public TaikoScoreProcessor() - : base(new TaikoRuleset()) + private const double combo_base = 4; + + protected override double ClassicScoreMultiplier => 22; + + public TaikoScoreProcessor(Ruleset ruleset) + : base(ruleset) { } - protected override double DefaultAccuracyPortion => 0.75; + protected override double ComputeTotalScore() + { + return + (int)Math.Round + (( + 250000 * ComboPortion / MaxComboPortion + + 750000 * Math.Pow(Accuracy.Value, 3.6) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier); + } - protected override double DefaultComboPortion => 0.25; + protected override void AddScoreChange(JudgementResult result) + { + var change = computeScoreChange(result); + BonusPortion += change.bonus; + ComboPortion += change.combo; + } - protected override double ClassicScoreMultiplier => 22; + protected override void RemoveScoreChange(JudgementResult result) + { + var change = computeScoreChange(result); + BonusPortion -= change.bonus; + ComboPortion -= change.combo; + } + + private (double combo, double bonus) computeScoreChange(JudgementResult result) + { + double hitValue = Judgement.ToNumericResult(result.Type); + + if (result.HitObject is StrongNestedHitObject strong) + { + double strongBonus = strong.Parent is DrumRollTick ? 3 : 7; + hitValue *= strongBonus; + } + + if (result.Type.IsBonus()) + return (0, hitValue); + + return (hitValue * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0); + } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index a35fdb890da3..599d0dc33c7e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -42,7 +42,7 @@ public class TaikoRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); + public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TaikoHealthProcessor(); diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs index 1c505ea107a3..cb23164c002a 100644 --- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs +++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs @@ -157,7 +157,8 @@ public void UpdateScore() Accuracy.Value = frame.Header.Accuracy; Combo.Value = frame.Header.Combo; - TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo); + // Todo: + // TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 4f802a22a1d1..03bd0f750998 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -66,7 +66,9 @@ private Task getPerfectPerformance(ScoreInfo score, Cance // calculate total score ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = perfectPlay.Mods; - perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); + + // Todo: + // perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); // compute rank achieved // default to SS, then adjust the rank with mods diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index d4223a80c234..13b8ad5d844e 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -65,7 +65,9 @@ private double getAccuracyWithImminentResultAdded(JudgementResult result) scoreProcessor.PopulateScore(score); score.Statistics[result.Type]++; - return scoreProcessor.ComputeAccuracy(score); + // Todo: + return 0; + // return scoreProcessor.ComputeAccuracy(score); } } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index a77068eb145c..2e7a58b96c29 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -232,7 +232,7 @@ protected Ruleset() /// Creates a for this . /// /// The score processor. - public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this); + public virtual ScoreProcessor CreateScoreProcessor() => new DefaultScoreProcessor(this); /// /// Creates a for this . @@ -381,4 +381,23 @@ protected Ruleset() /// public virtual RulesetSetupSection? CreateEditorSetupSection() => null; } + + public partial class DefaultScoreProcessor : ScoreProcessor + { + public DefaultScoreProcessor(Ruleset ruleset) + : base(ruleset) + { + } + + protected override double ComputeTotalScore() + { + return + (int)Math.Round + (( + 700000 * ComboPortion / MaxComboPortion + + 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier); + } + } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 96f69222249c..8373ebd50c3f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -4,11 +4,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.Contracts; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Localisation; @@ -20,8 +18,10 @@ namespace osu.Game.Rulesets.Scoring { - public partial class ScoreProcessor : JudgementProcessor + public abstract partial class ScoreProcessor : JudgementProcessor { + protected const double MAX_SCORE = 1000000; + private const double accuracy_cutoff_x = 1; private const double accuracy_cutoff_s = 0.95; private const double accuracy_cutoff_a = 0.9; @@ -29,8 +29,6 @@ public partial class ScoreProcessor : JudgementProcessor private const double accuracy_cutoff_c = 0.7; private const double accuracy_cutoff_d = 0; - private const double max_score = 1000000; - /// /// Invoked when this was reset from a replay frame. /// @@ -90,59 +88,65 @@ public partial class ScoreProcessor : JudgementProcessor public IReadOnlyList HitEvents => hitEvents; /// - /// The default portion of awarded for hitting s accurately. Defaults to 30%. + /// An arbitrary multiplier to scale scores in the scoring mode. /// - protected virtual double DefaultAccuracyPortion => 0.3; + protected virtual double ClassicScoreMultiplier => 36; /// - /// The default portion of awarded for achieving a high combo. Default to 70%. + /// The ruleset this score processor is valid for. /// - protected virtual double DefaultComboPortion => 0.7; + public readonly Ruleset Ruleset; /// - /// An arbitrary multiplier to scale scores in the scoring mode. + /// The sum of all basic judgements at the current time. /// - protected virtual double ClassicScoreMultiplier => 36; + private double currentBasicScore; /// - /// The ruleset this score processor is valid for. + /// The maximum sum of basic judgements at the current time. /// - public readonly Ruleset Ruleset; - - private readonly double accuracyPortion; - private readonly double comboPortion; + private double currentMaxBasicScore; - public Dictionary MaximumStatistics - { - get - { - if (!beatmapApplied) - throw new InvalidOperationException($"Cannot access maximum statistics before calling {nameof(ApplyBeatmap)}."); + /// + /// The total count of basic judgements in the beatmap. + /// + protected int MaxBasicJudgements { get; private set; } - return new Dictionary(maximumResultCounts); - } - } + /// + /// The current count of basic judgements by the player. + /// + protected int CurrentBasicJudgements { get; private set; } - private ScoringValues maximumScoringValues; + /// + /// The current combo score. + /// + protected double ComboPortion { get; set; } /// - /// Scoring values for the current play assuming all perfect hits. + /// The maximum achievable combo score. /// - /// - /// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session. - /// - private ScoringValues currentMaximumScoringValues; + protected double MaxComboPortion { get; private set; } /// - /// Scoring values for the current play. + /// The current bonus score. /// - private ScoringValues currentScoringValues; + protected double BonusPortion { get; set; } /// - /// The maximum of a basic (non-tick and non-bonus) hitobject. - /// Only populated via or . + /// The total score multiplier. /// - private HitResult? maxBasicResult; + protected double ScoreMultiplier { get; private set; } = 1; + + public Dictionary MaximumStatistics + { + get + { + if (!beatmapApplied) + throw new InvalidOperationException($"Cannot access maximum statistics before calling {nameof(ApplyBeatmap)}."); + + return new Dictionary(maximumResultCounts); + } + } private bool beatmapApplied; @@ -152,18 +156,10 @@ public Dictionary MaximumStatistics private readonly List hitEvents = new List(); private HitObject? lastHitObject; - private double scoreMultiplier = 1; - - public ScoreProcessor(Ruleset ruleset) + protected ScoreProcessor(Ruleset ruleset) { Ruleset = ruleset; - accuracyPortion = DefaultAccuracyPortion; - comboPortion = DefaultComboPortion; - - if (!Precision.AlmostEquals(1.0, accuracyPortion + comboPortion)) - throw new InvalidOperationException($"{nameof(DefaultAccuracyPortion)} + {nameof(DefaultComboPortion)} must equal 1."); - Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Accuracy.ValueChanged += accuracy => { @@ -175,10 +171,10 @@ public ScoreProcessor(Ruleset ruleset) Mode.ValueChanged += _ => updateScore(); Mods.ValueChanged += mods => { - scoreMultiplier = 1; + ScoreMultiplier = 1; foreach (var m in mods.NewValue) - scoreMultiplier *= m.ScoreMultiplier; + ScoreMultiplier *= m.ScoreMultiplier; updateScore(); }; @@ -200,10 +196,6 @@ protected sealed override void ApplyResultInternal(JudgementResult result) scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; - // Always update the maximum scoring values. - applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues); - currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0; - if (!result.Type.IsScorable()) return; @@ -212,8 +204,13 @@ protected sealed override void ApplyResultInternal(JudgementResult result) else if (result.Type.BreaksCombo()) Combo.Value = 0; - applyResult(result.Type, ref currentScoringValues); - currentScoringValues.MaxCombo = HighestCombo.Value; + if (result.Type.IsBasic()) + CurrentBasicJudgements++; + + currentMaxBasicScore += Judgement.ToNumericResult(result.Judgement.MaxResult); + currentBasicScore += Judgement.ToNumericResult(result.Type); + + AddScoreChange(result); hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; @@ -221,20 +218,6 @@ protected sealed override void ApplyResultInternal(JudgementResult result) updateScore(); } - private static void applyResult(HitResult result, ref ScoringValues scoringValues) - { - if (!result.IsScorable()) - return; - - if (result.IsBonus()) - scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0; - else - scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0; - - if (result.IsBasic()) - scoringValues.CountBasicHitObjects++; - } - /// /// Creates the that describes a . /// @@ -253,15 +236,16 @@ protected sealed override void RevertResultInternal(JudgementResult result) scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; - // Always update the maximum scoring values. - revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues); - currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0; - if (!result.Type.IsScorable()) return; - revertResult(result.Type, ref currentScoringValues); - currentScoringValues.MaxCombo = HighestCombo.Value; + if (result.Type.IsBasic()) + CurrentBasicJudgements--; + + currentMaxBasicScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); + currentBasicScore -= Judgement.ToNumericResult(result.Type); + + RemoveScoreChange(result); Debug.Assert(hitEvents.Count > 0); lastHitObject = hitEvents[^1].LastHitObject; @@ -270,111 +254,31 @@ protected sealed override void RevertResultInternal(JudgementResult result) updateScore(); } - private static void revertResult(HitResult result, ref ScoringValues scoringValues) + protected virtual void AddScoreChange(JudgementResult result) { - if (!result.IsScorable()) - return; - - if (result.IsBonus()) - scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0; + if (result.Type.IsBonus()) + BonusPortion += Judgement.ToNumericResult(result.Type); else - scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0; - - if (result.IsBasic()) - scoringValues.CountBasicHitObjects--; - } - - private void updateScore() - { - Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; - MinimumAccuracy.Value = maximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 0; - MaximumAccuracy.Value = maximumScoringValues.BaseScore > 0 - ? (double)(currentScoringValues.BaseScore + (maximumScoringValues.BaseScore - currentMaximumScoringValues.BaseScore)) / maximumScoringValues.BaseScore - : 1; - TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues); + ComboPortion += Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d); } - /// - /// Computes the accuracy of a given . - /// - /// The to compute the total score of. - /// The score's accuracy. - [Pure] - public double ComputeAccuracy(ScoreInfo scoreInfo) + protected virtual void RemoveScoreChange(JudgementResult result) { - if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - - // We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap. - extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); - - return maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1; + if (result.Type.IsBonus()) + BonusPortion -= Judgement.ToNumericResult(result.Type); + else + ComboPortion -= Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d); } - /// - /// Computes the total score of a given . - /// - /// - /// 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 . - [Pure] - public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) + private void updateScore() { - if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - - extractScoringValues(scoreInfo, out var current, out var maximum); + Accuracy.Value = currentMaxBasicScore > 0 ? currentBasicScore / currentMaxBasicScore : 1; - return computeScore(mode, current, maximum); + // Todo: Classic/Standardised + TotalScore.Value = (long)Math.Round(ComputeTotalScore()); } - /// - /// Computes the total score from scoring values. - /// - /// The to represent the score as. - /// The current scoring values. - /// The maximum scoring values. - /// The total score computed from the given scoring values. - [Pure] - private long computeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum) - { - double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1; - double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1; - return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects); - } - - /// - /// Computes the total score from individual scoring components. - /// - /// The to represent the score as. - /// The accuracy percentage achieved by the player. - /// The portion of the max combo achieved by the player. - /// 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. - [Pure] - public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects) - { - double accuracyScore = accuracyPortion * accuracyRatio; - double comboScore = comboPortion * comboRatio; - double rawScore = (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; - - switch (mode) - { - default: - case ScoringMode.Standardised: - return (long)Math.Round(rawScore); - - 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 scaledRawScore = rawScore / max_score; - return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier); - } - } + protected abstract double ComputeTotalScore(); /// /// Resets this ScoreProcessor to a default state. @@ -389,7 +293,8 @@ protected override void Reset(bool storeResults) if (storeResults) { - maximumScoringValues = currentScoringValues; + MaxComboPortion = ComboPortion; + MaxBasicJudgements = CurrentBasicJudgements; maximumResultCounts.Clear(); maximumResultCounts.AddRange(scoreResultCounts); @@ -397,8 +302,11 @@ protected override void Reset(bool storeResults) scoreResultCounts.Clear(); - currentScoringValues = default; - currentMaximumScoringValues = default; + currentBasicScore = 0; + currentMaxBasicScore = 0; + CurrentBasicJudgements = 0; + ComboPortion = 0; + BonusPortion = 0; TotalScore.Value = 0; Accuracy.Value = 1; @@ -406,6 +314,9 @@ protected override void Reset(bool storeResults) Rank.Disabled = false; Rank.Value = ScoreRank.X; HighestCombo.Value = 0; + + currentBasicScore = 0; + currentMaxBasicScore = 0; } /// @@ -428,7 +339,7 @@ public virtual void PopulateScore(ScoreInfo score) score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. - score.TotalScore = ComputeScore(ScoringMode.Standardised, score); + score.TotalScore = TotalScore.Value; } /// @@ -452,12 +363,6 @@ public override void ResetFromReplayFrame(ReplayFrame frame) if (frame.Header == null) return; - extractScoringValues(frame.Header.Statistics, out var current, out var maximum); - currentScoringValues.BaseScore = current.BaseScore; - currentScoringValues.MaxCombo = frame.Header.MaxCombo; - currentMaximumScoringValues.BaseScore = maximum.BaseScore; - currentMaximumScoringValues.MaxCombo = maximum.MaxCombo; - Combo.Value = frame.Header.Combo; HighestCombo.Value = frame.Header.MaxCombo; @@ -469,105 +374,6 @@ public override void ResetFromReplayFrame(ReplayFrame frame) OnResetFromReplayFrame?.Invoke(); } - #region ScoringValue extraction - - /// - /// Applies a best-effort extraction of hit statistics into . - /// - /// - /// This method is useful in a variety of situations, with a few drawbacks that need to be considered: - /// - /// The maximum will always be 0. - /// The current and maximum will always be the same value. - /// - /// Consumers are expected to more accurately fill in the above values through external means. - /// - /// Ensure to fill in the maximum for use in - /// . - /// - /// - /// The score to extract scoring values from. - /// The "current" scoring values, representing the hit statistics as they appear. - /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time. - [Pure] - private void extractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum) - { - extractScoringValues(scoreInfo.Statistics, out current, out maximum); - current.MaxCombo = scoreInfo.MaxCombo; - - if (scoreInfo.MaximumStatistics.Count > 0) - extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum); - } - - /// - /// Applies a best-effort extraction of hit statistics into . - /// - /// - /// This method is useful in a variety of situations, with a few drawbacks that need to be considered: - /// - /// The current will always be 0. - /// The maximum will always be 0. - /// The current and maximum will always be the same value. - /// - /// Consumers are expected to more accurately fill in the above values (especially the current ) via external means (e.g. ). - /// - /// The hit statistics to extract scoring values from. - /// The "current" scoring values, representing the hit statistics as they appear. - /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time. - [Pure] - private void extractScoringValues(IReadOnlyDictionary statistics, out ScoringValues current, out ScoringValues maximum) - { - current = default; - maximum = default; - - foreach ((HitResult result, int count) in statistics) - { - if (!result.IsScorable()) - continue; - - if (result.IsBonus()) - current.BonusScore += count * Judgement.ToNumericResult(result); - - if (result.AffectsAccuracy()) - { - // The maximum result of this judgement if it wasn't a miss. - // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT). - HitResult maxResult; - - switch (result) - { - case HitResult.LargeTickHit: - case HitResult.LargeTickMiss: - maxResult = HitResult.LargeTickHit; - break; - - case HitResult.SmallTickHit: - case HitResult.SmallTickMiss: - maxResult = HitResult.SmallTickHit; - break; - - default: - maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result; - break; - } - - current.BaseScore += count * Judgement.ToNumericResult(result); - maximum.BaseScore += count * Judgement.ToNumericResult(maxResult); - } - - if (result.AffectsCombo()) - maximum.MaxCombo += count; - - if (result.IsBasic()) - { - current.CountBasicHitObjects += count; - maximum.CountBasicHitObjects += count; - } - } - } - - #endregion - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -629,32 +435,6 @@ public static double AccuracyCutoffFromRank(ScoreRank rank) } #endregion - - /// - /// Stores the required scoring data that fulfils the minimum requirements for a to calculate score. - /// - private struct ScoringValues - { - /// - /// The sum of all "basic" scoring values. See: and . - /// - public long BaseScore; - - /// - /// The sum of all "bonus" scoring values. See: and . - /// - public long BonusScore; - - /// - /// The highest achieved combo. - /// - public int MaxCombo; - - /// - /// The count of "basic" s. See: . - /// - public int CountBasicHitObjects; - } } public enum ScoringMode diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3e6d09b74a04..3779156fda65 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -115,7 +115,9 @@ public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringM var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - return scoreProcessor.ComputeScore(mode, score); + // Todo: + return 0; + // return scoreProcessor.ComputeScore(mode, score); } /// diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 0c25a3225947..7b77785c7aea 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -67,7 +67,8 @@ protected override async Task PrepareScoreForResultsAsync(Score score) { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo); + // Todo: + // Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo); } protected override void Dispose(bool isDisposing) From a7b623f52af08fd05d89366f169fe75031fa14c3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 9 May 2023 20:00:14 +0900 Subject: [PATCH 03/41] Reimplement classic scoring mode --- .../Scoring/CatchScoreProcessor.cs | 12 +++++------- .../Scoring/ManiaScoreProcessor.cs | 12 +++++------- .../Scoring/OsuScoreProcessor.cs | 12 +++++------- .../Scoring/TaikoScoreProcessor.cs | 12 +++++------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 16 ++++++++++++++-- osu.Game/Scoring/ScoreManager.cs | 8 +++----- 6 files changed, 37 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index bf5c3a91d644..9c5359ebeb88 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -31,13 +31,11 @@ protected override double ComputeTotalScore() const int tiny_droplets_portion = 400000; - return - (int)Math.Round - (( - ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * ComboPortion / MaxComboPortion + - tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + - BonusPortion - ) * ScoreMultiplier); + return ( + ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * ComboPortion / MaxComboPortion + + tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + + BonusPortion + ) * ScoreMultiplier; } protected override void AddScoreChange(JudgementResult result) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 7d188c678646..19f8a4a6397f 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -20,13 +20,11 @@ public ManiaScoreProcessor(Ruleset ruleset) protected override double ComputeTotalScore() { - return - (int)Math.Round - (( - 200000 * ComboPortion / MaxComboPortion + - 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + - BonusPortion - ) * ScoreMultiplier); + return ( + 200000 * ComboPortion / MaxComboPortion + + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier; } protected override void AddScoreChange(JudgementResult result) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 5f5997b0c1b3..f8cbf1a64122 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -17,13 +17,11 @@ public OsuScoreProcessor(Ruleset ruleset) protected override double ComputeTotalScore() { - return - (int)Math.Round - (( - 700000 * ComboPortion / MaxComboPortion + - 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + - BonusPortion - ) * ScoreMultiplier); + return ( + 700000 * ComboPortion / MaxComboPortion + + 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier; } } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index caf0a799a283..71eb0b160273 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -21,13 +21,11 @@ public TaikoScoreProcessor(Ruleset ruleset) protected override double ComputeTotalScore() { - return - (int)Math.Round - (( - 250000 * ComboPortion / MaxComboPortion + - 750000 * Math.Pow(Accuracy.Value, 3.6) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + - BonusPortion - ) * ScoreMultiplier); + return ( + 250000 * ComboPortion / MaxComboPortion + + 750000 * Math.Pow(Accuracy.Value, 3.6) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier; } protected override void AddScoreChange(JudgementResult result) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 8373ebd50c3f..9172034ff6b9 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -274,8 +274,20 @@ private void updateScore() { Accuracy.Value = currentMaxBasicScore > 0 ? currentBasicScore / currentMaxBasicScore : 1; - // Todo: Classic/Standardised - TotalScore.Value = (long)Math.Round(ComputeTotalScore()); + double standardisedScore = ComputeTotalScore(); + + if (Mode.Value == ScoringMode.Standardised) + TotalScore.Value = (long)Math.Round(standardisedScore); + else + TotalScore.Value = ConvertToClassic(standardisedScore); + } + + public long ConvertToClassic(double standardised) + { + // 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 scaledRawScore = standardised / MAX_SCORE; + return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, MaxBasicJudgements), 2) * ClassicScoreMultiplier); } protected abstract double ComputeTotalScore(); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3779156fda65..0674947f306f 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -107,17 +107,15 @@ public IEnumerable OrderByTotalScore(IEnumerable scores) /// The total score. public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised) { - // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place. - if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash)) + if (mode == ScoringMode.Standardised) return score.TotalScore; var ruleset = score.Ruleset.CreateInstance(); var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - // Todo: - return 0; - // return scoreProcessor.ComputeScore(mode, score); + // Todo: This loses precision because we're dealing with pre-rounded total scores. + return scoreProcessor.ConvertToClassic(score.TotalScore); } /// From f2483a1cf8c4f91fd8c9374b803e2bd553fe68f4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 17:27:47 +0900 Subject: [PATCH 04/41] Add some helper methods, fix precision differences Introduces some error at all times, but if we're to store scores everywhere as `long`, then the same precision should be applied to the "during gameplay" path as well. --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 40 ++++++++++++++++----- osu.Game/Scoring/IScoreInfo.cs | 3 ++ osu.Game/Scoring/ScoreManager.cs | 10 +++--- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9172034ff6b9..cbcf0dceaf5b 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -274,20 +274,44 @@ private void updateScore() { Accuracy.Value = currentMaxBasicScore > 0 ? currentBasicScore / currentMaxBasicScore : 1; - double standardisedScore = ComputeTotalScore(); + long standardisedScore = (long)Math.Round(ComputeTotalScore()); - if (Mode.Value == ScoringMode.Standardised) - TotalScore.Value = (long)Math.Round(standardisedScore); - else - TotalScore.Value = ConvertToClassic(standardisedScore); + TotalScore.Value = Mode.Value == ScoringMode.Standardised + ? standardisedScore + : convertToClassic(standardisedScore, MaxBasicJudgements, ClassicScoreMultiplier); + } + + /// + /// Retrieves the total score from a in the given scoring mode. + /// + /// The mode to return the total score in. + /// The score to get the total score of. + /// The total score. + public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) + { + int maxBasicJudgements = scoreInfo.MaximumStatistics.Where(k => k.Key.IsBasic()) + .Select(k => k.Value) + .DefaultIfEmpty(0) + .Sum(); + + return mode == ScoringMode.Standardised + ? scoreInfo.TotalScore + : convertToClassic(scoreInfo.TotalScore, maxBasicJudgements, ClassicScoreMultiplier); } - public long ConvertToClassic(double standardised) + /// + /// Converts a standardised total score to the classic score. + /// + /// The standardised score. + /// The maximum possible number of basic judgements. + /// The classic multiplier. + /// The classic score. + private static long convertToClassic(long score, int maxBasicJudgements, double classicMultiplier) { // 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 scaledRawScore = standardised / MAX_SCORE; - return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, MaxBasicJudgements), 2) * ClassicScoreMultiplier); + double scaledRawScore = score / MAX_SCORE; + return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * classicMultiplier); } protected abstract double ComputeTotalScore(); diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index 289679a7244a..ffc30384d211 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -15,6 +15,9 @@ public interface IScoreInfo : IHasOnlineID, IHasNamedFiles { IUser User { get; } + /// + /// The standardised total score. + /// long TotalScore { get; } int MaxCombo { get; } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 0674947f306f..f616c6db6d72 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -107,15 +107,13 @@ public IEnumerable OrderByTotalScore(IEnumerable scores) /// The total score. public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised) { + // Shortcut to avoid potentially creating many ruleset objects in the default scoring mode. if (mode == ScoringMode.Standardised) return score.TotalScore; - var ruleset = score.Ruleset.CreateInstance(); - var scoreProcessor = ruleset.CreateScoreProcessor(); - scoreProcessor.Mods.Value = score.Mods; - - // Todo: This loses precision because we're dealing with pre-rounded total scores. - return scoreProcessor.ConvertToClassic(score.TotalScore); + return score.Ruleset.CreateInstance() + .CreateScoreProcessor() + .ComputeScore(mode, score); } /// From 829e47d30b279ca949b9b2f02d57724a9048e27e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 17:47:25 +0900 Subject: [PATCH 05/41] Add MaxTotalScore for performance breakdown calculator --- .../Rulesets/Difficulty/PerformanceBreakdownCalculator.cs | 6 +++--- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 03bd0f750998..3dd7f934a800 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -62,13 +62,13 @@ private Task getPerfectPerformance(ScoreInfo score, Cance .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) .ToDictionary(pair => pair.hitResult, pair => pair.count); perfectPlay.Statistics = statistics; + perfectPlay.MaximumStatistics = statistics; // calculate total score ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = perfectPlay.Mods; - - // Todo: - // perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); + scoreProcessor.ApplyBeatmap(playableBeatmap); + perfectPlay.TotalScore = scoreProcessor.MaxTotalScore; // 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 cbcf0dceaf5b..ebecc39aa735 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -97,6 +97,11 @@ public abstract partial class ScoreProcessor : JudgementProcessor /// public readonly Ruleset Ruleset; + /// + /// The maximum achievable total score. + /// + public long MaxTotalScore { get; private set; } + /// /// The sum of all basic judgements at the current time. /// @@ -334,6 +339,8 @@ protected override void Reset(bool storeResults) maximumResultCounts.Clear(); maximumResultCounts.AddRange(scoreResultCounts); + + MaxTotalScore = TotalScore.Value; } scoreResultCounts.Clear(); From 510b8e4c7887df92574ec0963154082a9ebb8347 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 18:53:43 +0900 Subject: [PATCH 06/41] Remove ScoreManager.Mode, handle per-use --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 48 +------------------ .../Scoring/Legacy/ScoreInfoExtensions.cs | 45 +++++++++++++++++ osu.Game/Scoring/ScoreManager.cs | 12 +---- .../Play/HUD/GameplayLeaderboardScore.cs | 18 ++++++- .../Screens/Play/HUD/GameplayScoreCounter.cs | 5 +- .../Screens/Play/HUD/ILeaderboardScore.cs | 4 ++ .../Play/HUD/SoloGameplayLeaderboard.cs | 18 ++----- osu.Game/Screens/Play/Player.cs | 3 -- 8 files changed, 77 insertions(+), 76 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ebecc39aa735..bda2d7140d3f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Scoring { public abstract partial class ScoreProcessor : JudgementProcessor { - protected const double MAX_SCORE = 1000000; + public const double MAX_SCORE = 1000000; private const double accuracy_cutoff_x = 1; private const double accuracy_cutoff_s = 0.95; @@ -76,11 +76,6 @@ public abstract partial class ScoreProcessor : JudgementProcessor /// public readonly BindableInt HighestCombo = new BindableInt(); - /// - /// The used to calculate scores. - /// - public readonly Bindable Mode = new Bindable(); - /// /// The s collected during gameplay thus far. /// Intended for use with various statistics displays. @@ -173,7 +168,6 @@ protected ScoreProcessor(Ruleset ruleset) Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); }; - Mode.ValueChanged += _ => updateScore(); Mods.ValueChanged += mods => { ScoreMultiplier = 1; @@ -278,45 +272,7 @@ protected virtual void RemoveScoreChange(JudgementResult result) private void updateScore() { Accuracy.Value = currentMaxBasicScore > 0 ? currentBasicScore / currentMaxBasicScore : 1; - - long standardisedScore = (long)Math.Round(ComputeTotalScore()); - - TotalScore.Value = Mode.Value == ScoringMode.Standardised - ? standardisedScore - : convertToClassic(standardisedScore, MaxBasicJudgements, ClassicScoreMultiplier); - } - - /// - /// Retrieves the total score from a in the given scoring mode. - /// - /// The mode to return the total score in. - /// The score to get the total score of. - /// The total score. - public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) - { - int maxBasicJudgements = scoreInfo.MaximumStatistics.Where(k => k.Key.IsBasic()) - .Select(k => k.Value) - .DefaultIfEmpty(0) - .Sum(); - - return mode == ScoringMode.Standardised - ? scoreInfo.TotalScore - : convertToClassic(scoreInfo.TotalScore, maxBasicJudgements, ClassicScoreMultiplier); - } - - /// - /// Converts a standardised total score to the classic score. - /// - /// The standardised score. - /// The maximum possible number of basic judgements. - /// The classic multiplier. - /// The classic score. - private static long convertToClassic(long score, int maxBasicJudgements, double classicMultiplier) - { - // 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 scaledRawScore = score / MAX_SCORE; - return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * classicMultiplier); + TotalScore.Value = (long)Math.Round(ComputeTotalScore()); } protected abstract double ComputeTotalScore(); diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index e42f6caf2639..af991c7ea337 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -3,13 +3,58 @@ #nullable disable +using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring.Legacy { public static class ScoreInfoExtensions { + public static long GetDisplayScore(this ScoreProcessor scoreProcessor, ScoringMode mode) + => getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics); + + public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode) + => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); + + private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) + { + if (mode == ScoringMode.Standardised) + return score; + + double multiplier; + + switch (rulesetId) + { + case 0: + multiplier = 36; + break; + + case 1: + multiplier = 22; + break; + + case 2: + multiplier = 28; + break; + + case 3: + multiplier = 16; + break; + + default: + return score; + } + + int maxBasicJudgements = maximumStatistics.Where(k => k.Key.IsBasic()).Select(k => k.Value).DefaultIfEmpty(0).Sum(); + + // 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 scaledRawScore = score / ScoreProcessor.MAX_SCORE; + return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * multiplier); + } + public static int? GetCountGeki(this ScoreInfo scoreInfo) { switch (scoreInfo.Ruleset.OnlineID) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index f616c6db6d72..fa5a9fc7c16d 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -20,6 +20,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Online.API; +using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring { @@ -105,16 +106,7 @@ public IEnumerable OrderByTotalScore(IEnumerable scores) /// The to calculate the total score of. /// The to return the total score as. /// The total score. - public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised) - { - // Shortcut to avoid potentially creating many ruleset objects in the default scoring mode. - if (mode == ScoringMode.Standardised) - return score.TotalScore; - - return score.Ruleset.CreateInstance() - .CreateScoreProcessor() - .ComputeScore(mode, score); - } + public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised) => score.GetDisplayScore(mode); /// /// Retrieves the maximum achievable combo for the provided score. diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 07b80feb3e42..74a39254355d 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -11,8 +11,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; @@ -55,6 +57,7 @@ public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardS public BindableInt Combo { get; } = new BindableInt(); public BindableBool HasQuit { get; } = new BindableBool(); public Bindable DisplayOrder { get; } = new Bindable(); + public Func GetDisplayScore { get; set; } public Color4? BackgroundColour { get; set; } @@ -100,6 +103,8 @@ public int? ScorePosition private Container scoreComponents; + private IBindable scoreDisplayMode; + /// /// Creates a new . /// @@ -112,10 +117,12 @@ public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked) AutoSizeAxes = Axes.X; Height = PANEL_HEIGHT; + + GetDisplayScore = _ => TotalScore.Value; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OsuConfigManager osuConfigManager) { Container avatarContainer; @@ -286,7 +293,9 @@ private void load(OsuColour colours) LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); - TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); + scoreDisplayMode = osuConfigManager.GetBindable(OsuSetting.ScoreDisplayMode); + scoreDisplayMode.BindValueChanged(_ => updateScore()); + TotalScore.BindValueChanged(_ => updateScore(), true); Accuracy.BindValueChanged(v => { @@ -303,6 +312,11 @@ private void load(OsuColour colours) HasQuit.BindValueChanged(_ => updateState()); } + private void updateScore() + { + scoreText.Text = GetDisplayScore(scoreDisplayMode.Value).ToString("N0"); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs index a11cccd97c9e..a696d2cad7d2 100644 --- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs @@ -9,12 +9,14 @@ using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring.Legacy; namespace osu.Game.Screens.Play.HUD { public abstract partial class GameplayScoreCounter : ScoreCounter { private Bindable scoreDisplayMode; + private Bindable totalScoreBindable; protected GameplayScoreCounter() : base(6) @@ -42,7 +44,8 @@ private void load(OsuConfigManager config, ScoreProcessor scoreProcessor) } }, true); - Current.BindTo(scoreProcessor.TotalScore); + totalScoreBindable = scoreProcessor.TotalScore.GetBoundCopy(); + totalScoreBindable.BindValueChanged(_ => Current.Value = scoreProcessor.GetDisplayScore(scoreDisplayMode.Value), true); } } } diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs index 428390f90cc8..cc1d83e0c708 100644 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using osu.Framework.Bindables; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { @@ -20,5 +22,7 @@ public interface ILeaderboardScore /// Lower numbers will appear higher in cases of ties. /// Bindable DisplayOrder { get; } + + Func GetDisplayScore { get; set; } } } diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index 9f9288091939..e9bb1d210199 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -10,6 +9,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Select; using osu.Game.Users; @@ -27,15 +27,9 @@ public partial class SoloGameplayLeaderboard : GameplayLeaderboard public readonly IBindableList Scores = new BindableList(); - // hold references to ensure bindables are updated. - private readonly List> scoreBindables = new List>(); - [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - /// /// Whether the leaderboard should be visible regardless of the configuration value. /// This is true by default, but can be changed. @@ -70,7 +64,6 @@ protected override void LoadComplete() private void showScores() { Clear(); - scoreBindables.Clear(); if (!Scores.Any()) return; @@ -79,12 +72,8 @@ private void showScores() { var score = Add(s.User, false); - var bindableTotal = scoreManager.GetBindableTotalScore(s); - - // Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298). - bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true); - scoreBindables.Add(bindableTotal); - + score.GetDisplayScore = s.GetDisplayScore; + score.TotalScore.Value = s.TotalScore; score.Accuracy.Value = s.Accuracy; score.Combo.Value = s.MaxCombo; score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds(); @@ -92,6 +81,7 @@ private void showScores() ILeaderboardScore local = Add(trackingUser, true); + local.GetDisplayScore = scoreProcessor.GetDisplayScore; local.TotalScore.BindTarget = scoreProcessor.TotalScore; local.Accuracy.BindTarget = scoreProcessor.Accuracy; local.Combo.BindTarget = scoreProcessor.HighestCombo; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5174adfc063c..18ea9d0acbf7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -237,9 +237,6 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game, dependencies.CacheAs(HealthProcessor); - if (!ScoreProcessor.Mode.Disabled) - config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); From f3591f83a2d831a737f094afed372c365c3a40a5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 18:55:10 +0900 Subject: [PATCH 07/41] Remove ScoreManager.GetTotalScore() --- osu.Game/Scoring/ScoreManager.cs | 17 ++++------------- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index fa5a9fc7c16d..d5509538fd1b 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -75,7 +75,7 @@ public ScoreInfo Query(Expression> query) /// The array of s to reorder. /// The given ordered by decreasing total score. public IEnumerable OrderByTotalScore(IEnumerable scores) - => scores.OrderByDescending(s => GetTotalScore(s)) + => scores.OrderByDescending(s => s.TotalScore) .ThenBy(s => s.OnlineID) // Local scores may not have an online ID. Fall back to date in these cases. .ThenBy(s => s.Date); @@ -88,7 +88,7 @@ public IEnumerable OrderByTotalScore(IEnumerable scores) /// /// The to retrieve the bindable for. /// The bindable containing the total score. - public Bindable GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager); + public Bindable GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, configManager); /// /// Retrieves a bindable that represents the formatted total score string of a . @@ -100,14 +100,6 @@ public IEnumerable OrderByTotalScore(IEnumerable scores) /// The bindable containing the formatted total score string. public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); - /// - /// Retrieves the total score of a in the given . - /// - /// The to calculate the total score of. - /// The to return the total score as. - /// The total score. - public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised) => score.GetDisplayScore(mode); - /// /// Retrieves the maximum achievable combo for the provided score. /// @@ -126,12 +118,11 @@ private class TotalScoreBindable : Bindable /// Creates a new . /// /// The to provide the total score of. - /// The . /// The config. - public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager) + public TotalScoreBindable(ScoreInfo score, OsuConfigManager configManager) { configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); - scoringMode.BindValueChanged(mode => Value = scoreManager.GetTotalScore(score, mode.NewValue), true); + scoringMode.BindValueChanged(mode => Value = score.GetDisplayScore(mode.NewValue), true); } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 29dec4208311..1f93389e94ea 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -149,7 +149,7 @@ private void displayScore(ScorePanelTrackingContainer trackingContainer) var score = trackingContainer.Panel.Score; - flow.SetLayoutPosition(trackingContainer, scoreManager.GetTotalScore(score)); + flow.SetLayoutPosition(trackingContainer, score.TotalScore); trackingContainer.Show(); From 808818768bf46710191b7071f67f512078cddd2e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 19:02:49 +0900 Subject: [PATCH 08/41] Add TotalScore to replay frame headers --- osu.Game/Online/Spectator/FrameHeader.cs | 10 +++++++++- osu.Game/Online/Spectator/SpectatorScoreProcessor.cs | 4 +--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index b6dcd8aaa594..4d1ff23530fd 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -44,6 +44,12 @@ public class FrameHeader [Key(4)] public DateTimeOffset ReceivedTime { get; set; } + /// + /// The total score. + /// + [Key(5)] + public long TotalScore { get; set; } + /// /// Construct header summary information from a point-in-time reference to a score which is actively being played. /// @@ -53,6 +59,7 @@ public FrameHeader(ScoreInfo score) Combo = score.Combo; MaxCombo = score.MaxCombo; Accuracy = score.Accuracy; + TotalScore = score.TotalScore; // copy for safety Statistics = new Dictionary(score.Statistics); @@ -60,13 +67,14 @@ public FrameHeader(ScoreInfo score) [JsonConstructor] [SerializationConstructor] - public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime) + public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime, long totalScore) { Combo = combo; MaxCombo = maxCombo; Accuracy = accuracy; Statistics = statistics; ReceivedTime = receivedTime; + TotalScore = totalScore; } } } diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs index cb23164c002a..90b584a89f9d 100644 --- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs +++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs @@ -153,12 +153,10 @@ public void UpdateScore() scoreInfo.MaxCombo = frame.Header.MaxCombo; scoreInfo.Statistics = frame.Header.Statistics; scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics; + scoreInfo.TotalScore = frame.Header.TotalScore; Accuracy.Value = frame.Header.Accuracy; Combo.Value = frame.Header.Combo; - - // Todo: - // TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo); } protected override void Dispose(bool isDisposing) From c33e4fe75ea22c129bfbae9a5df268d8036a1ddb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 20:10:28 +0900 Subject: [PATCH 09/41] Remove unnecessary override --- .../Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 7b77785c7aea..b0e458598678 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -6,14 +6,12 @@ using System; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; 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; @@ -63,14 +61,6 @@ protected override ResultsScreen CreateResults(ScoreInfo score) return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, true); } - protected override async Task PrepareScoreForResultsAsync(Score score) - { - await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - - // Todo: - // Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From ef86be6d21c56a3350bc0969705a91074d7b112d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 20:29:26 +0900 Subject: [PATCH 10/41] Fix base score added for non-accuracy-affecting objects --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 44 ++++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index bda2d7140d3f..1162b6ac0437 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -98,14 +98,20 @@ public abstract partial class ScoreProcessor : JudgementProcessor public long MaxTotalScore { get; private set; } /// - /// The sum of all basic judgements at the current time. + /// The sum of all accuracy-affecting judgements at the current time. /// - private double currentBasicScore; + /// + /// Used to compute accuracy. + /// + private double currentBaseScore; /// - /// The maximum sum of basic judgements at the current time. + /// The maximum sum of accuracy-affecting judgements at the current time. /// - private double currentMaxBasicScore; + /// + /// Used to compute accuracy. + /// + private double currentMaxBaseScore; /// /// The total count of basic judgements in the beatmap. @@ -206,8 +212,11 @@ protected sealed override void ApplyResultInternal(JudgementResult result) if (result.Type.IsBasic()) CurrentBasicJudgements++; - currentMaxBasicScore += Judgement.ToNumericResult(result.Judgement.MaxResult); - currentBasicScore += Judgement.ToNumericResult(result.Type); + if (result.Type.AffectsAccuracy()) + { + currentMaxBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult); + currentBaseScore += Judgement.ToNumericResult(result.Type); + } AddScoreChange(result); @@ -241,8 +250,11 @@ protected sealed override void RevertResultInternal(JudgementResult result) if (result.Type.IsBasic()) CurrentBasicJudgements--; - currentMaxBasicScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); - currentBasicScore -= Judgement.ToNumericResult(result.Type); + if (result.Type.AffectsAccuracy()) + { + currentMaxBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); + currentBaseScore -= Judgement.ToNumericResult(result.Type); + } RemoveScoreChange(result); @@ -257,7 +269,8 @@ protected virtual void AddScoreChange(JudgementResult result) { if (result.Type.IsBonus()) BonusPortion += Judgement.ToNumericResult(result.Type); - else + + if (result.Type.AffectsCombo()) ComboPortion += Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d); } @@ -265,13 +278,14 @@ protected virtual void RemoveScoreChange(JudgementResult result) { if (result.Type.IsBonus()) BonusPortion -= Judgement.ToNumericResult(result.Type); - else + + if (result.Type.AffectsCombo()) ComboPortion -= Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d); } private void updateScore() { - Accuracy.Value = currentMaxBasicScore > 0 ? currentBasicScore / currentMaxBasicScore : 1; + Accuracy.Value = currentMaxBaseScore > 0 ? currentBaseScore / currentMaxBaseScore : 1; TotalScore.Value = (long)Math.Round(ComputeTotalScore()); } @@ -301,8 +315,8 @@ protected override void Reset(bool storeResults) scoreResultCounts.Clear(); - currentBasicScore = 0; - currentMaxBasicScore = 0; + currentBaseScore = 0; + currentMaxBaseScore = 0; CurrentBasicJudgements = 0; ComboPortion = 0; BonusPortion = 0; @@ -314,8 +328,8 @@ protected override void Reset(bool storeResults) Rank.Value = ScoreRank.X; HighestCombo.Value = 0; - currentBasicScore = 0; - currentMaxBasicScore = 0; + currentBaseScore = 0; + currentMaxBaseScore = 0; } /// From 00e0411369a14e724705fcc35f2088832aa91bcb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 20:37:13 +0900 Subject: [PATCH 11/41] Fix/rework ModAccuracyChallenge calculation --- .../Rulesets/Mods/ModAccuracyChallenge.cs | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index 13b8ad5d844e..57284b7eaad3 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -42,9 +42,29 @@ public class ModAccuracyChallenge : ModFailCondition, IApplicableToScoreProcesso Value = 0.9, }; - private ScoreProcessor scoreProcessor = null!; + private int baseScore; + private int maxBaseScore; - public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) => this.scoreProcessor = scoreProcessor; + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + scoreProcessor.NewJudgement += j => + { + if (!j.Type.AffectsAccuracy()) + return; + + baseScore += Judgement.ToNumericResult(j.Type); + maxBaseScore += Judgement.ToNumericResult(j.Judgement.MaxResult); + }; + + scoreProcessor.JudgementReverted += j => + { + if (!j.Type.AffectsAccuracy()) + return; + + baseScore -= Judgement.ToNumericResult(j.Type); + maxBaseScore -= Judgement.ToNumericResult(j.Judgement.MaxResult); + }; + } public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; @@ -58,16 +78,11 @@ protected override bool FailCondition(HealthProcessor healthProcessor, Judgement private double getAccuracyWithImminentResultAdded(JudgementResult result) { - var score = new ScoreInfo { Ruleset = scoreProcessor.Ruleset.RulesetInfo }; - - // This is super ugly, but if we don't do it this way we will not have the most recent result added to the accuracy value. - // Hopefully we can improve this in the future. - scoreProcessor.PopulateScore(score); - score.Statistics[result.Type]++; + // baseScore and maxBaseScore are always exactly one judgement behind because the health processor is processed first (see: Player). + int imminentBaseScore = baseScore + Judgement.ToNumericResult(result.Type); + int imminentMaxBaseScore = maxBaseScore + Judgement.ToNumericResult(result.Judgement.MaxResult); - // Todo: - return 0; - // return scoreProcessor.ComputeAccuracy(score); + return imminentMaxBaseScore > 0 ? imminentBaseScore / (double)imminentMaxBaseScore : 1; } } } From 8b56a3f87d568dfd2c9fdf63b8bd534181b18d2f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 May 2023 20:52:40 +0900 Subject: [PATCH 12/41] Remove ClassicScoreMultiplier and DefaultScoreProcessor --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Scoring/CatchScoreProcessor.cs | 8 +++---- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Scoring/ManiaScoreProcessor.cs | 8 +++---- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Scoring/OsuScoreProcessor.cs | 8 +++---- .../Scoring/TaikoScoreProcessor.cs | 8 +++---- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 21 +------------------ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 20 +++++++++++------- 10 files changed, 29 insertions(+), 52 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index e2255fa6f73b..8a0b8250d561 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -34,7 +34,7 @@ public class CatchRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableCatchRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); + public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 9c5359ebeb88..435b39150b95 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -13,15 +13,13 @@ public partial class CatchScoreProcessor : ScoreProcessor private const int combo_cap = 200; private const double combo_base = 4; - protected override double ClassicScoreMultiplier => 28; - private double tinyDropletScale; private int maximumTinyDroplets; private int hitTinyDroplets; - public CatchScoreProcessor(Ruleset ruleset) - : base(ruleset) + public CatchScoreProcessor() + : base(new CatchRuleset()) { } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index c6065c9b9615..d324682989f3 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -50,7 +50,7 @@ public class ManiaRuleset : Ruleset, ILegacyRuleset public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableManiaRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime); diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 19f8a4a6397f..1c55f9ffced0 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -11,10 +11,8 @@ public partial class ManiaScoreProcessor : ScoreProcessor { private const double combo_base = 4; - protected override double ClassicScoreMultiplier => 16; - - public ManiaScoreProcessor(Ruleset ruleset) - : base(ruleset) + public ManiaScoreProcessor() + : base(new ManiaRuleset()) { } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 497d40543657..922594a93ae2 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -45,7 +45,7 @@ public class OsuRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableOsuRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this); + public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index f8cbf1a64122..d4677d92c193 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -8,10 +8,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { public partial class OsuScoreProcessor : ScoreProcessor { - protected override double ClassicScoreMultiplier => 36; - - public OsuScoreProcessor(Ruleset ruleset) - : base(ruleset) + public OsuScoreProcessor() + : base(new OsuRuleset()) { } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 71eb0b160273..e2b442739b30 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -12,10 +12,8 @@ public partial class TaikoScoreProcessor : ScoreProcessor { private const double combo_base = 4; - protected override double ClassicScoreMultiplier => 22; - - public TaikoScoreProcessor(Ruleset ruleset) - : base(ruleset) + public TaikoScoreProcessor() + : base(new TaikoRuleset()) { } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 599d0dc33c7e..a35fdb890da3 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -42,7 +42,7 @@ public class TaikoRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); + public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TaikoHealthProcessor(); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 2e7a58b96c29..a77068eb145c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -232,7 +232,7 @@ protected Ruleset() /// Creates a for this . /// /// The score processor. - public virtual ScoreProcessor CreateScoreProcessor() => new DefaultScoreProcessor(this); + public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this); /// /// Creates a for this . @@ -381,23 +381,4 @@ protected Ruleset() /// public virtual RulesetSetupSection? CreateEditorSetupSection() => null; } - - public partial class DefaultScoreProcessor : ScoreProcessor - { - public DefaultScoreProcessor(Ruleset ruleset) - : base(ruleset) - { - } - - protected override double ComputeTotalScore() - { - return - (int)Math.Round - (( - 700000 * ComboPortion / MaxComboPortion + - 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + - BonusPortion - ) * ScoreMultiplier); - } - } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 1162b6ac0437..4ded3d5d91cf 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Scoring { - public abstract partial class ScoreProcessor : JudgementProcessor + public partial class ScoreProcessor : JudgementProcessor { public const double MAX_SCORE = 1000000; @@ -82,11 +82,6 @@ public abstract partial class ScoreProcessor : JudgementProcessor /// public IReadOnlyList HitEvents => hitEvents; - /// - /// An arbitrary multiplier to scale scores in the scoring mode. - /// - protected virtual double ClassicScoreMultiplier => 36; - /// /// The ruleset this score processor is valid for. /// @@ -162,7 +157,7 @@ public Dictionary MaximumStatistics private readonly List hitEvents = new List(); private HitObject? lastHitObject; - protected ScoreProcessor(Ruleset ruleset) + public ScoreProcessor(Ruleset ruleset) { Ruleset = ruleset; @@ -289,7 +284,16 @@ private void updateScore() TotalScore.Value = (long)Math.Round(ComputeTotalScore()); } - protected abstract double ComputeTotalScore(); + protected virtual double ComputeTotalScore() + { + return + (int)Math.Round + (( + 700000 * ComboPortion / MaxComboPortion + + 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + BonusPortion + ) * ScoreMultiplier); + } /// /// Resets this ScoreProcessor to a default state. From 035d0d5c9ce27d79d84ffee0b24b89064699b538 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 13:16:57 +0900 Subject: [PATCH 13/41] Fix multiplayer leaderboard not working --- .../Spectator/SpectatorScoreProcessor.cs | 25 ++++++++++--------- .../HUD/MultiplayerGameplayLeaderboard.cs | 1 + 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs index 90b584a89f9d..3242e219945b 100644 --- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs +++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; namespace osu.Game.Online.Spectator { @@ -46,7 +47,9 @@ public partial class SpectatorScoreProcessor : Component /// /// The applied s. /// - public IReadOnlyList Mods => scoreProcessor?.Mods.Value ?? Array.Empty(); + public IReadOnlyList Mods => scoreInfo?.Mods ?? Array.Empty(); + + public Func GetDisplayScore => mode => scoreInfo?.GetDisplayScore(mode) ?? 0; private IClock? referenceClock; @@ -70,7 +73,6 @@ public IClock ReferenceClock private readonly int userId; private SpectatorState? spectatorState; - private ScoreProcessor? scoreProcessor; private ScoreInfo? scoreInfo; public SpectatorScoreProcessor(int userId) @@ -94,19 +96,15 @@ private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEve { if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null) { - scoreProcessor?.RemoveAndDisposeImmediately(); - scoreProcessor = null; scoreInfo = null; spectatorState = null; replayFrames.Clear(); return; } - if (scoreProcessor != null) + if (scoreInfo != null) return; - Debug.Assert(scoreInfo == null); - RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value); if (rulesetInfo == null) return; @@ -114,9 +112,11 @@ private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEve Ruleset ruleset = rulesetInfo.CreateInstance(); spectatorState = userState; - scoreInfo = new ScoreInfo { Ruleset = rulesetInfo }; - scoreProcessor = ruleset.CreateScoreProcessor(); - scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray(); + scoreInfo = new ScoreInfo + { + Ruleset = rulesetInfo, + Mods = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray() + }; } private void onNewFrames(int incomingUserId, FrameDataBundle bundle) @@ -126,7 +126,7 @@ private void onNewFrames(int incomingUserId, FrameDataBundle bundle) Schedule(() => { - if (scoreProcessor == null) + if (scoreInfo == null) return; replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); @@ -140,7 +140,6 @@ public void UpdateScore() return; Debug.Assert(spectatorState != null); - Debug.Assert(scoreProcessor != null); int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime)); if (frameIndex < 0) @@ -150,6 +149,7 @@ public void UpdateScore() TimedFrame frame = replayFrames[frameIndex]; Debug.Assert(frame.Header != null); + scoreInfo.Accuracy = frame.Header.Accuracy; scoreInfo.MaxCombo = frame.Header.MaxCombo; scoreInfo.Statistics = frame.Header.Statistics; scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics; @@ -157,6 +157,7 @@ public void UpdateScore() Accuracy.Value = frame.Header.Accuracy; Combo.Value = frame.Header.Combo; + TotalScore.Value = frame.Header.TotalScore; } 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 620f3718c270..922def617441 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -98,6 +98,7 @@ private void load(OsuConfigManager config, IAPIProvider api, CancellationToken c var trackedUser = UserScores[user.Id]; var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id); + leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore; leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy); leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore); leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo); From 4d14467d95cf3b0a1cab08b70a5408d587f056f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 13:23:04 +0900 Subject: [PATCH 14/41] Invert order of operations --- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index e2b442739b30..f86a6669ca53 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -29,15 +29,15 @@ protected override double ComputeTotalScore() protected override void AddScoreChange(JudgementResult result) { var change = computeScoreChange(result); - BonusPortion += change.bonus; ComboPortion += change.combo; + BonusPortion += change.bonus; } protected override void RemoveScoreChange(JudgementResult result) { var change = computeScoreChange(result); - BonusPortion -= change.bonus; ComboPortion -= change.combo; + BonusPortion -= change.bonus; } private (double combo, double bonus) computeScoreChange(JudgementResult result) From 73544231de1c75263e0dc116890b5119350abc7a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 14:06:46 +0900 Subject: [PATCH 15/41] Fix TestSceneTopLocalRank --- osu.Game.Tests/Resources/TestResources.cs | 2 +- .../Visual/SongSelect/TestSceneTopLocalRank.cs | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index a2d81c0a7565..a77dc8d49b90 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -179,7 +179,7 @@ public static ScoreInfo CreateTestScoreInfo(RulesetInfo ruleset = null) => BeatmapHash = beatmap.Hash, Ruleset = beatmap.Ruleset, Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }, - TotalScore = 2845370, + TotalScore = 284537, Accuracy = 0.95, MaxCombo = 999, Position = 1, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index cf0de14541f4..79baae53e8cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -12,7 +11,6 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Select.Carousel; using osu.Game.Tests.Resources; @@ -143,25 +141,20 @@ public void TestLegacyScore() testScoreInfo.User = API.LocalUser.Value; testScoreInfo.Rank = ScoreRank.B; - testScoreInfo.TotalScore = scoreManager.GetTotalScore(testScoreInfo, ScoringMode.Classic); scoreManager.Import(testScoreInfo); }); AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B); - AddStep("Add higher score for current user", () => + AddStep("Add higher-graded score for current user", () => { var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap); testScoreInfo2.User = API.LocalUser.Value; testScoreInfo2.Rank = ScoreRank.X; testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics; - testScoreInfo2.TotalScore = scoreManager.GetTotalScore(testScoreInfo2); - - // ensure second score has a total score (standardised) less than first one (classic) - // despite having better statistics, otherwise this test is pointless. - Debug.Assert(testScoreInfo2.TotalScore < testScoreInfo.TotalScore); + testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1; scoreManager.Import(testScoreInfo2); }); From 9a1d749020e365fe0519e5ca06a6b551370c4aa6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 14:06:53 +0900 Subject: [PATCH 16/41] Fix TestSceneDrawableTaikoMascot --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index e4e68c7207f3..dd8748f6e3ff 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -91,8 +91,9 @@ public void TestIdleState() { prepareDrawableRulesetAndBeatmap(false); - assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); - assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); + var hit = new Hit(); + assertStateAfterResult(new JudgementResult(hit, new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(hit), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); } [Test] From 7cbf48ffcff775dcfa77a5787fa72d0f89df7d8e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 14:09:19 +0900 Subject: [PATCH 17/41] Fix TestSceneScoring and incorrect combo calculations --- .../Scoring/CatchScoreProcessor.cs | 2 +- .../Scoring/ManiaScoreProcessor.cs | 2 +- .../Scoring/TaikoScoreProcessor.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs | 11 ++++++----- osu.Game/Rulesets/Judgements/JudgementResult.cs | 5 +++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 ++++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 435b39150b95..ae3966ccc5a2 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -60,7 +60,7 @@ protected override void RemoveScoreChange(JudgementResult result) if (result.Type.IsBonus()) return (0, Judgement.ToNumericResult(result.Type), 0); - return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(combo_cap, combo_base)), 0, 0); + return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)), 0, 0); } protected override void Reset(bool storeResults) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 1c55f9ffced0..4c8f8ef65e51 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -44,7 +44,7 @@ protected override void RemoveScoreChange(JudgementResult result) if (result.Type.IsBonus()) return (0, Judgement.ToNumericResult(result.Type)); - return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0); + return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)), 0); } } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index f86a6669ca53..2ecd962d720b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -53,7 +53,7 @@ protected override void RemoveScoreChange(JudgementResult result) if (result.Type.IsBonus()) return (0, hitValue); - return (hitValue * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0); + return (hitValue * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)), 0); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs index 8fff07e6d85e..2b378c80138c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring.Legacy; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -124,8 +125,8 @@ private void rerun() graphs.Clear(); legend.Clear(); - runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } }); - runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } }); + runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised); + runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic); runScoreV1(); runScoreV2(); @@ -218,7 +219,7 @@ void applyHitV2(int baseScore) }); } - private void runForProcessor(string name, Color4 colour, ScoreProcessor processor) + private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode) { int maxCombo = sliderMaxCombo.Current.Value; @@ -232,10 +233,10 @@ private void runForProcessor(string name, Color4 colour, ScoreProcessor processo () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }), () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }), () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }), - () => (int)processor.TotalScore.Value); + () => processor.GetDisplayScore(mode)); } - private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore) + private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore) { int maxCombo = sliderMaxCombo.Current.Value; diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index bf29919e3422..34d1f1f6e902 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -64,6 +64,11 @@ public double TimeOffset /// public int ComboAtJudgement { get; internal set; } + /// + /// The combo after this occurred. + /// + public int ComboAfterJudgement { get; internal set; } + /// /// The highest combo achieved prior to this occurring. /// diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 4ded3d5d91cf..b014b12297f3 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -204,6 +204,8 @@ protected sealed override void ApplyResultInternal(JudgementResult result) else if (result.Type.BreaksCombo()) Combo.Value = 0; + result.ComboAfterJudgement = Combo.Value; + if (result.Type.IsBasic()) CurrentBasicJudgements++; @@ -266,7 +268,7 @@ protected virtual void AddScoreChange(JudgementResult result) BonusPortion += Judgement.ToNumericResult(result.Type); if (result.Type.AffectsCombo()) - ComboPortion += Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d); + ComboPortion += Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d); } protected virtual void RemoveScoreChange(JudgementResult result) @@ -275,7 +277,7 @@ protected virtual void RemoveScoreChange(JudgementResult result) BonusPortion -= Judgement.ToNumericResult(result.Type); if (result.Type.AffectsCombo()) - ComboPortion -= Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d); + ComboPortion -= Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d); } private void updateScore() From 2ae34530f787e9420bdaefda85583bc165984075 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 14:14:34 +0900 Subject: [PATCH 18/41] Avoid NaN values during ApplyBeatmap processing() --- osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs | 3 ++- osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs | 7 +++++-- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 7 +++++-- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 7 +++++-- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 +++++-- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index ae3966ccc5a2..dc6d5b28eebc 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -25,12 +25,13 @@ public CatchScoreProcessor() protected override double ComputeTotalScore() { + double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; double fruitHitsRatio = maximumTinyDroplets == 0 ? 0 : (double)hitTinyDroplets / maximumTinyDroplets; const int tiny_droplets_portion = 400000; return ( - ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * ComboPortion / MaxComboPortion + + ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * comboRatio + tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + BonusPortion ) * ScoreMultiplier; diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 4c8f8ef65e51..544b9add322f 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -18,9 +18,12 @@ public ManiaScoreProcessor() protected override double ComputeTotalScore() { + double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; + double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; + return ( - 200000 * ComboPortion / MaxComboPortion + - 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + 200000 * comboRatio + + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyRatio + BonusPortion ) * ScoreMultiplier; } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index d4677d92c193..edd088027125 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -15,9 +15,12 @@ public OsuScoreProcessor() protected override double ComputeTotalScore() { + double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; + double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; + return ( - 700000 * ComboPortion / MaxComboPortion + - 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + 700000 * comboRatio + + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyRatio + BonusPortion ) * ScoreMultiplier; } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 2ecd962d720b..32f4421ed25c 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -19,9 +19,12 @@ public TaikoScoreProcessor() protected override double ComputeTotalScore() { + double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; + double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; + return ( - 250000 * ComboPortion / MaxComboPortion + - 750000 * Math.Pow(Accuracy.Value, 3.6) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + 250000 * comboRatio + + 750000 * Math.Pow(Accuracy.Value, 3.6) * accuracyRatio + BonusPortion ) * ScoreMultiplier; } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b014b12297f3..0ec9c884c36f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -288,11 +288,14 @@ private void updateScore() protected virtual double ComputeTotalScore() { + double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; + double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; + return (int)Math.Round (( - 700000 * ComboPortion / MaxComboPortion + - 300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) + + 700000 * comboRatio + + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyRatio + BonusPortion ) * ScoreMultiplier); } From d74bf2a096575a0f55f74f417724ad4d3593f8cf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 14:37:26 +0900 Subject: [PATCH 19/41] Refactor for safety --- .../Scoring/CatchScoreProcessor.cs | 51 ++++----- .../Scoring/ManiaScoreProcessor.cs | 36 +----- .../Scoring/OsuScoreProcessor.cs | 13 +-- .../Scoring/TaikoScoreProcessor.cs | 42 ++----- .../PerformanceBreakdownCalculator.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 107 ++++++++++-------- 6 files changed, 105 insertions(+), 146 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index dc6d5b28eebc..c74d4b84e443 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -17,51 +17,48 @@ public partial class CatchScoreProcessor : ScoreProcessor private int maximumTinyDroplets; private int hitTinyDroplets; + private int maximumBasicJudgements; + private int currentBasicJudgements; public CatchScoreProcessor() : base(new CatchRuleset()) { } - protected override double ComputeTotalScore() + protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) { - double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; double fruitHitsRatio = maximumTinyDroplets == 0 ? 0 : (double)hitTinyDroplets / maximumTinyDroplets; const int tiny_droplets_portion = 400000; - return ( - ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * comboRatio + - tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + - BonusPortion - ) * ScoreMultiplier; + return ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * comboRatio + + tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + + bonusPortion; } - protected override void AddScoreChange(JudgementResult result) + protected override double GetComboScoreChange(JudgementResult result) + => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); + + protected override void ApplyScoreChange(JudgementResult result) { - var change = computeScoreChange(result); - ComboPortion += change.combo; - BonusPortion += change.bonus; - hitTinyDroplets += change.tinyDropletHits; + base.ApplyScoreChange(result); + + if (result.HitObject is TinyDroplet) + hitTinyDroplets++; + + if (result.Type.IsBasic()) + currentBasicJudgements++; } protected override void RemoveScoreChange(JudgementResult result) { - var change = computeScoreChange(result); - ComboPortion -= change.combo; - BonusPortion -= change.bonus; - hitTinyDroplets -= change.tinyDropletHits; - } + base.RemoveScoreChange(result); - private (double combo, double bonus, int tinyDropletHits) computeScoreChange(JudgementResult result) - { if (result.HitObject is TinyDroplet) - return (0, 0, 1); - - if (result.Type.IsBonus()) - return (0, Judgement.ToNumericResult(result.Type), 0); + hitTinyDroplets--; - return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)), 0, 0); + if (result.Type.IsBasic()) + currentBasicJudgements--; } protected override void Reset(bool storeResults) @@ -71,14 +68,16 @@ protected override void Reset(bool storeResults) if (storeResults) { maximumTinyDroplets = hitTinyDroplets; + maximumBasicJudgements = currentBasicJudgements; - if (maximumTinyDroplets + MaxBasicJudgements == 0) + if (maximumTinyDroplets + maximumBasicJudgements == 0) tinyDropletScale = 0; else - tinyDropletScale = (double)maximumTinyDroplets / (maximumTinyDroplets + MaxBasicJudgements); + tinyDropletScale = (double)maximumTinyDroplets / (maximumTinyDroplets + maximumBasicJudgements); } hitTinyDroplets = 0; + currentBasicJudgements = 0; } } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 544b9add322f..214aded2d74d 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -16,38 +16,14 @@ public ManiaScoreProcessor() { } - protected override double ComputeTotalScore() + protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) { - double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; - double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; - - return ( - 200000 * comboRatio + - 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyRatio + - BonusPortion - ) * ScoreMultiplier; - } - - protected override void AddScoreChange(JudgementResult result) - { - var change = computeScoreChange(result); - ComboPortion += change.combo; - BonusPortion += change.bonus; - } - - protected override void RemoveScoreChange(JudgementResult result) - { - var change = computeScoreChange(result); - ComboPortion -= change.combo; - BonusPortion -= change.bonus; + return 200000 * comboRatio + + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyRatio + + bonusPortion; } - private (double combo, double bonus) computeScoreChange(JudgementResult result) - { - if (result.Type.IsBonus()) - return (0, Judgement.ToNumericResult(result.Type)); - - return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)), 0); - } + protected override double GetComboScoreChange(JudgementResult result) + => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)); } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index edd088027125..5028d5b8d6bd 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -13,16 +13,11 @@ public OsuScoreProcessor() { } - protected override double ComputeTotalScore() + protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) { - double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; - double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; - - return ( - 700000 * comboRatio + - 300000 * Math.Pow(Accuracy.Value, 10) * accuracyRatio + - BonusPortion - ) * ScoreMultiplier; + return 700000 * comboRatio + + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyRatio + + bonusPortion; } } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 32f4421ed25c..e0f83505cf81 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -17,46 +17,28 @@ public TaikoScoreProcessor() { } - protected override double ComputeTotalScore() + protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) { - double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; - double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; - - return ( - 250000 * comboRatio + - 750000 * Math.Pow(Accuracy.Value, 3.6) * accuracyRatio + - BonusPortion - ) * ScoreMultiplier; + return 250000 * comboRatio + + 750000 * Math.Pow(Accuracy.Value, 3.6) * accuracyRatio + + bonusPortion; } - protected override void AddScoreChange(JudgementResult result) - { - var change = computeScoreChange(result); - ComboPortion += change.combo; - BonusPortion += change.bonus; - } + protected override double GetBonusScoreChange(JudgementResult result) => base.GetBonusScoreChange(result) * strongScaleValue(result); - protected override void RemoveScoreChange(JudgementResult result) + protected override double GetComboScoreChange(JudgementResult result) { - var change = computeScoreChange(result); - ComboPortion -= change.combo; - BonusPortion -= change.bonus; + return Judgement.ToNumericResult(result.Type) + * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)) + * strongScaleValue(result); } - private (double combo, double bonus) computeScoreChange(JudgementResult result) + private double strongScaleValue(JudgementResult result) { - double hitValue = Judgement.ToNumericResult(result.Type); - if (result.HitObject is StrongNestedHitObject strong) - { - double strongBonus = strong.Parent is DrumRollTick ? 3 : 7; - hitValue *= strongBonus; - } - - if (result.Type.IsBonus()) - return (0, hitValue); + return strong.Parent is DrumRollTick ? 3 : 7; - return (hitValue * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)), 0); + return 1; } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 3dd7f934a800..64a04f896faa 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -68,7 +68,7 @@ private Task getPerfectPerformance(ScoreInfo score, Cance ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = perfectPlay.Mods; scoreProcessor.ApplyBeatmap(playableBeatmap); - perfectPlay.TotalScore = scoreProcessor.MaxTotalScore; + perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore; // 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 0ec9c884c36f..a94ee9c18115 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -90,53 +90,53 @@ public partial class ScoreProcessor : JudgementProcessor /// /// The maximum achievable total score. /// - public long MaxTotalScore { get; private set; } + public long MaximumTotalScore { get; private set; } /// - /// The sum of all accuracy-affecting judgements at the current time. + /// The maximum sum of accuracy-affecting judgements at the current point in time. /// /// /// Used to compute accuracy. /// - private double currentBaseScore; + private double currentMaximumBaseScore; /// - /// The maximum sum of accuracy-affecting judgements at the current time. + /// The sum of all accuracy-affecting judgements at the current point in time. /// /// /// Used to compute accuracy. /// - private double currentMaxBaseScore; + private double currentBaseScore; /// - /// The total count of basic judgements in the beatmap. + /// The count of all basic judgements in the beatmap. /// - protected int MaxBasicJudgements { get; private set; } + private int maximumCountBasicJudgements; /// - /// The current count of basic judgements by the player. + /// The count of basic judgements at the current point in time. /// - protected int CurrentBasicJudgements { get; private set; } + private int currentCountBasicJudgements; /// - /// The current combo score. + /// The maximum combo score in the beatmap. /// - protected double ComboPortion { get; set; } + private double maximumComboPortion; /// - /// The maximum achievable combo score. + /// The combo score at the current point in time. /// - protected double MaxComboPortion { get; private set; } + private double currentComboPortion; /// - /// The current bonus score. + /// The bonus score at the current point in time. /// - protected double BonusPortion { get; set; } + private double currentBonusPortion; /// /// The total score multiplier. /// - protected double ScoreMultiplier { get; private set; } = 1; + private double scoreMultiplier = 1; public Dictionary MaximumStatistics { @@ -171,10 +171,10 @@ public ScoreProcessor(Ruleset ruleset) Mods.ValueChanged += mods => { - ScoreMultiplier = 1; + scoreMultiplier = 1; foreach (var m in mods.NewValue) - ScoreMultiplier *= m.ScoreMultiplier; + scoreMultiplier *= m.ScoreMultiplier; updateScore(); }; @@ -207,15 +207,21 @@ protected sealed override void ApplyResultInternal(JudgementResult result) result.ComboAfterJudgement = Combo.Value; if (result.Type.IsBasic()) - CurrentBasicJudgements++; + currentCountBasicJudgements++; if (result.Type.AffectsAccuracy()) { - currentMaxBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult); + currentMaximumBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult); currentBaseScore += Judgement.ToNumericResult(result.Type); } - AddScoreChange(result); + if (result.Type.IsBonus()) + currentBonusPortion += GetBonusScoreChange(result); + + if (result.Type.AffectsCombo()) + currentComboPortion += GetComboScoreChange(result); + + ApplyScoreChange(result); hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; @@ -245,14 +251,20 @@ protected sealed override void RevertResultInternal(JudgementResult result) return; if (result.Type.IsBasic()) - CurrentBasicJudgements--; + currentCountBasicJudgements--; if (result.Type.AffectsAccuracy()) { - currentMaxBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); + currentMaximumBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); currentBaseScore -= Judgement.ToNumericResult(result.Type); } + if (result.Type.IsBonus()) + currentBonusPortion -= GetBonusScoreChange(result); + + if (result.Type.AffectsCombo()) + currentComboPortion -= GetComboScoreChange(result); + RemoveScoreChange(result); Debug.Assert(hitEvents.Count > 0); @@ -262,42 +274,37 @@ protected sealed override void RevertResultInternal(JudgementResult result) updateScore(); } - protected virtual void AddScoreChange(JudgementResult result) - { - if (result.Type.IsBonus()) - BonusPortion += Judgement.ToNumericResult(result.Type); + protected virtual double GetBonusScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type); - if (result.Type.AffectsCombo()) - ComboPortion += Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d); + protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d); + + protected virtual void ApplyScoreChange(JudgementResult result) + { } protected virtual void RemoveScoreChange(JudgementResult result) { - if (result.Type.IsBonus()) - BonusPortion -= Judgement.ToNumericResult(result.Type); - - if (result.Type.AffectsCombo()) - ComboPortion -= Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d); } private void updateScore() { - Accuracy.Value = currentMaxBaseScore > 0 ? currentBaseScore / currentMaxBaseScore : 1; - TotalScore.Value = (long)Math.Round(ComputeTotalScore()); + Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1; + + double comboRatio = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; + double accuracyRatio = maximumCountBasicJudgements > 0 ? (double)currentCountBasicJudgements / maximumCountBasicJudgements : 1; + + TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboRatio, accuracyRatio, currentBonusPortion) * scoreMultiplier); } - protected virtual double ComputeTotalScore() + protected virtual double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) { - double comboRatio = MaxComboPortion > 0 ? ComboPortion / MaxComboPortion : 1; - double accuracyRatio = MaxBasicJudgements > 0 ? (double)CurrentBasicJudgements / MaxBasicJudgements : 1; - return (int)Math.Round (( 700000 * comboRatio + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyRatio + - BonusPortion - ) * ScoreMultiplier); + bonusPortion + ) * scoreMultiplier); } /// @@ -313,22 +320,22 @@ protected override void Reset(bool storeResults) if (storeResults) { - MaxComboPortion = ComboPortion; - MaxBasicJudgements = CurrentBasicJudgements; + maximumComboPortion = currentComboPortion; + maximumCountBasicJudgements = currentCountBasicJudgements; maximumResultCounts.Clear(); maximumResultCounts.AddRange(scoreResultCounts); - MaxTotalScore = TotalScore.Value; + MaximumTotalScore = TotalScore.Value; } scoreResultCounts.Clear(); currentBaseScore = 0; - currentMaxBaseScore = 0; - CurrentBasicJudgements = 0; - ComboPortion = 0; - BonusPortion = 0; + currentMaximumBaseScore = 0; + currentCountBasicJudgements = 0; + currentComboPortion = 0; + currentBonusPortion = 0; TotalScore.Value = 0; Accuracy.Value = 1; @@ -338,7 +345,7 @@ protected override void Reset(bool storeResults) HighestCombo.Value = 0; currentBaseScore = 0; - currentMaxBaseScore = 0; + currentMaximumBaseScore = 0; } /// From 6c6f8621c1ae5b7ea26116b3883c9ae9683b8bde Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 16:25:52 +0900 Subject: [PATCH 20/41] Add score processor statistics to replay header --- .../Scoring/CatchScoreProcessor.cs | 17 ++++++++ osu.Game/Online/Spectator/FrameDataBundle.cs | 5 ++- osu.Game/Online/Spectator/FrameHeader.cs | 43 ++++++++++++------- osu.Game/Online/Spectator/SpectatorClient.cs | 6 ++- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 27 ++++++++++-- .../Visual/Spectator/TestSpectatorClient.cs | 7 ++- 6 files changed, 82 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index c74d4b84e443..63937600bb3b 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -79,5 +80,21 @@ protected override void Reset(bool storeResults) hitTinyDroplets = 0; currentBasicJudgements = 0; } + + public override void WriteScoreProcessorStatistics(IDictionary statistics) + { + base.WriteScoreProcessorStatistics(statistics); + + statistics.Add(nameof(hitTinyDroplets), hitTinyDroplets); + statistics.Add(nameof(currentBasicJudgements), currentBasicJudgements); + } + + public override void ReadScoreProcessorStatistics(IReadOnlyDictionary statistics) + { + base.ReadScoreProcessorStatistics(statistics); + + hitTinyDroplets = (int)statistics.GetValueOrDefault(nameof(hitTinyDroplets), 0); + currentBasicJudgements = (int)statistics.GetValueOrDefault(nameof(currentBasicJudgements), 0); + } } } diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index 97ae468875a9..b93684743432 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -6,6 +6,7 @@ using MessagePack; using Newtonsoft.Json; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Online.Spectator @@ -20,10 +21,10 @@ public class FrameDataBundle [Key(1)] public IList Frames { get; set; } - public FrameDataBundle(ScoreInfo score, IList frames) + public FrameDataBundle(ScoreInfo score, ScoreProcessor scoreProcessor, IList frames) { Frames = frames; - Header = new FrameHeader(score); + Header = new FrameHeader(score, scoreProcessor); } [JsonConstructor] diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index 4d1ff23530fd..baebb28e4f12 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -15,66 +15,77 @@ namespace osu.Game.Online.Spectator public class FrameHeader { /// - /// The current accuracy of the score. + /// The total score. /// [Key(0)] + public long TotalScore { get; set; } + + /// + /// The current accuracy of the score. + /// + [Key(1)] public double Accuracy { get; set; } /// /// The current combo of the score. /// - [Key(1)] + [Key(2)] public int Combo { get; set; } /// /// The maximum combo achieved up to the current point in time. /// - [Key(2)] + [Key(3)] public int MaxCombo { get; set; } /// /// Cumulative hit statistics. /// - [Key(3)] + [Key(4)] public Dictionary Statistics { get; set; } /// - /// The time at which this frame was received by the server. + /// Additional statistics that guides the score processor to calculate the correct score for this frame. /// - [Key(4)] - public DateTimeOffset ReceivedTime { get; set; } + [Key(5)] + public Dictionary ScoreProcessorStatistics { get; set; } /// - /// The total score. + /// The time at which this frame was received by the server. /// - [Key(5)] - public long TotalScore { get; set; } + [Key(6)] + public DateTimeOffset ReceivedTime { get; set; } /// /// Construct header summary information from a point-in-time reference to a score which is actively being played. /// /// The score for reference. - public FrameHeader(ScoreInfo score) + /// The score processor for reference. + public FrameHeader(ScoreInfo score, ScoreProcessor scoreProcessor) { + TotalScore = score.TotalScore; + Accuracy = score.Accuracy; Combo = score.Combo; MaxCombo = score.MaxCombo; - Accuracy = score.Accuracy; - TotalScore = score.TotalScore; // copy for safety Statistics = new Dictionary(score.Statistics); + + ScoreProcessorStatistics = new Dictionary(); + scoreProcessor.WriteScoreProcessorStatistics(ScoreProcessorStatistics); } [JsonConstructor] [SerializationConstructor] - public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime, long totalScore) + public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary statistics, Dictionary scoreProcessorStatistics, DateTimeOffset receivedTime) { + TotalScore = totalScore; + Accuracy = accuracy; Combo = combo; MaxCombo = maxCombo; - Accuracy = accuracy; Statistics = statistics; + ScoreProcessorStatistics = scoreProcessorStatistics; ReceivedTime = receivedTime; - TotalScore = totalScore; } } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 55ec75f4cec7..89da8b9d32b4 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -16,6 +16,7 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -82,6 +83,7 @@ public abstract partial class SpectatorClient : Component, ISpectatorClient private IBeatmap? currentBeatmap; private Score? currentScore; private long? currentScoreToken; + private ScoreProcessor? currentScoreProcessor; private readonly Queue pendingFrameBundles = new Queue(); @@ -192,6 +194,7 @@ public void BeginPlaying(long? scoreToken, GameplayState state, Score score) currentBeatmap = state.Beatmap; currentScore = score; currentScoreToken = scoreToken; + currentScoreProcessor = state.ScoreProcessor; BeginPlayingInternal(currentScoreToken, currentState); }); @@ -302,9 +305,10 @@ private void purgePendingFrames() return; Debug.Assert(currentScore != null); + Debug.Assert(currentScoreProcessor != null); var frames = pendingFrames.ToArray(); - var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames); + var bundle = new FrameDataBundle(currentScore.ScoreInfo, currentScoreProcessor, frames); pendingFrames.Clear(); lastPurgeTime = Time.Current; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index a94ee9c18115..0244e7f5e3c1 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -394,19 +394,34 @@ public override void ResetFromReplayFrame(ReplayFrame frame) Combo.Value = frame.Header.Combo; HighestCombo.Value = frame.Header.MaxCombo; + TotalScore.Value = frame.Header.TotalScore; scoreResultCounts.Clear(); scoreResultCounts.AddRange(frame.Header.Statistics); + ReadScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics); + updateScore(); OnResetFromReplayFrame?.Invoke(); } - protected override void Dispose(bool isDisposing) + public virtual void WriteScoreProcessorStatistics(IDictionary statistics) { - base.Dispose(isDisposing); - hitEvents.Clear(); + statistics.Add(nameof(currentMaximumBaseScore), currentMaximumBaseScore); + statistics.Add(nameof(currentBaseScore), currentBaseScore); + statistics.Add(nameof(currentCountBasicJudgements), currentCountBasicJudgements); + statistics.Add(nameof(currentComboPortion), currentComboPortion); + statistics.Add(nameof(currentBonusPortion), currentBonusPortion); + } + + public virtual void ReadScoreProcessorStatistics(IReadOnlyDictionary statistics) + { + currentMaximumBaseScore = (double)statistics.GetValueOrDefault(nameof(currentMaximumBaseScore), 0); + currentBaseScore = (double)statistics.GetValueOrDefault(nameof(currentBaseScore), 0); + currentCountBasicJudgements = (int)statistics.GetValueOrDefault(nameof(currentCountBasicJudgements), 0); + currentComboPortion = (double)statistics.GetValueOrDefault(nameof(currentComboPortion), 0); + currentBonusPortion = (double)statistics.GetValueOrDefault(nameof(currentBonusPortion), 0); } #region Static helper methods @@ -464,6 +479,12 @@ public static double AccuracyCutoffFromRank(ScoreRank rank) } #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + hitEvents.Clear(); + } } public enum ScoringMode diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1db35b3aaaba..305a615102e7 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -12,7 +12,9 @@ using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Tests.Visual.Spectator @@ -44,6 +46,9 @@ public partial class TestSpectatorClient : SpectatorClient [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + public TestSpectatorClient() { OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; @@ -119,7 +124,7 @@ void flush() if (frames.Count == 0) return; - var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, frames.ToArray()); + var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray()); ((ISpectatorClient)this).UserSentFrames(userId, bundle); frames.Clear(); From 30a296bd094604d5f2c7a2c3ddc1e9d03c60fa80 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 May 2023 17:27:02 +0900 Subject: [PATCH 21/41] Rename parameters --- osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs | 4 ++-- osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs | 6 +++--- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 6 +++--- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 6 +++--- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 63937600bb3b..9d5fc553de81 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -26,13 +26,13 @@ public CatchScoreProcessor() { } - protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { double fruitHitsRatio = maximumTinyDroplets == 0 ? 0 : (double)hitTinyDroplets / maximumTinyDroplets; const int tiny_droplets_portion = 400000; - return ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * comboRatio + return ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * comboProgress + tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + bonusPortion; } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 214aded2d74d..3341f834ddb3 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -16,10 +16,10 @@ public ManiaScoreProcessor() { } - protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return 200000 * comboRatio - + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyRatio + return 200000 * comboProgress + + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress + bonusPortion; } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 5028d5b8d6bd..ab07ac3e9de5 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -13,10 +13,10 @@ public OsuScoreProcessor() { } - protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return 700000 * comboRatio - + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyRatio + return 700000 * comboProgress + + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress + bonusPortion; } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index e0f83505cf81..a77e6db6f38f 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -17,10 +17,10 @@ public TaikoScoreProcessor() { } - protected override double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return 250000 * comboRatio - + 750000 * Math.Pow(Accuracy.Value, 3.6) * accuracyRatio + return 250000 * comboProgress + + 750000 * Math.Pow(Accuracy.Value, 3.6) * accuracyProgress + bonusPortion; } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 0244e7f5e3c1..d0bbe863fb32 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -296,13 +296,13 @@ private void updateScore() TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboRatio, accuracyRatio, currentBonusPortion) * scoreMultiplier); } - protected virtual double ComputeTotalScore(double comboRatio, double accuracyRatio, double bonusPortion) + protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { return (int)Math.Round (( - 700000 * comboRatio + - 300000 * Math.Pow(Accuracy.Value, 10) * accuracyRatio + + 700000 * comboProgress + + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress + bonusPortion ) * scoreMultiplier); } From 25d72d370e95c8e0722bbccb96b0cc01a78e61f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 20 May 2023 00:24:43 +0900 Subject: [PATCH 22/41] Always add non-bonus change to combo portion --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index d0bbe863fb32..f2b1607f5e43 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -217,8 +217,7 @@ protected sealed override void ApplyResultInternal(JudgementResult result) if (result.Type.IsBonus()) currentBonusPortion += GetBonusScoreChange(result); - - if (result.Type.AffectsCombo()) + else currentComboPortion += GetComboScoreChange(result); ApplyScoreChange(result); @@ -261,8 +260,7 @@ protected sealed override void RevertResultInternal(JudgementResult result) if (result.Type.IsBonus()) currentBonusPortion -= GetBonusScoreChange(result); - - if (result.Type.AffectsCombo()) + else currentComboPortion -= GetComboScoreChange(result); RemoveScoreChange(result); From c291d6fc829d02103230f4cb0881fe2bdeca1985 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 May 2023 12:02:44 +0900 Subject: [PATCH 23/41] Remove catch tiny droplet portion --- .../Scoring/CatchScoreProcessor.cs | 74 +------------------ 1 file changed, 2 insertions(+), 72 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 9d5fc553de81..9323296b7f9e 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -14,13 +12,6 @@ public partial class CatchScoreProcessor : ScoreProcessor private const int combo_cap = 200; private const double combo_base = 4; - private double tinyDropletScale; - - private int maximumTinyDroplets; - private int hitTinyDroplets; - private int maximumBasicJudgements; - private int currentBasicJudgements; - public CatchScoreProcessor() : base(new CatchRuleset()) { @@ -28,73 +19,12 @@ public CatchScoreProcessor() protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - double fruitHitsRatio = maximumTinyDroplets == 0 ? 0 : (double)hitTinyDroplets / maximumTinyDroplets; - - const int tiny_droplets_portion = 400000; - - return ((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * comboProgress - + tiny_droplets_portion * tinyDropletScale * fruitHitsRatio + return 600000 * comboProgress + + 400000 * Accuracy.Value * accuracyProgress + bonusPortion; } protected override double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); - - protected override void ApplyScoreChange(JudgementResult result) - { - base.ApplyScoreChange(result); - - if (result.HitObject is TinyDroplet) - hitTinyDroplets++; - - if (result.Type.IsBasic()) - currentBasicJudgements++; - } - - protected override void RemoveScoreChange(JudgementResult result) - { - base.RemoveScoreChange(result); - - if (result.HitObject is TinyDroplet) - hitTinyDroplets--; - - if (result.Type.IsBasic()) - currentBasicJudgements--; - } - - protected override void Reset(bool storeResults) - { - base.Reset(storeResults); - - if (storeResults) - { - maximumTinyDroplets = hitTinyDroplets; - maximumBasicJudgements = currentBasicJudgements; - - if (maximumTinyDroplets + maximumBasicJudgements == 0) - tinyDropletScale = 0; - else - tinyDropletScale = (double)maximumTinyDroplets / (maximumTinyDroplets + maximumBasicJudgements); - } - - hitTinyDroplets = 0; - currentBasicJudgements = 0; - } - - public override void WriteScoreProcessorStatistics(IDictionary statistics) - { - base.WriteScoreProcessorStatistics(statistics); - - statistics.Add(nameof(hitTinyDroplets), hitTinyDroplets); - statistics.Add(nameof(currentBasicJudgements), currentBasicJudgements); - } - - public override void ReadScoreProcessorStatistics(IReadOnlyDictionary statistics) - { - base.ReadScoreProcessorStatistics(statistics); - - hitTinyDroplets = (int)statistics.GetValueOrDefault(nameof(hitTinyDroplets), 0); - currentBasicJudgements = (int)statistics.GetValueOrDefault(nameof(currentBasicJudgements), 0); - } } } From e1feeded127aeb95b6a5f34c95c93ee0cd484b78 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 May 2023 12:49:41 +0900 Subject: [PATCH 24/41] Change statistics type, remove overridability --- osu.Game/Online/Spectator/FrameHeader.cs | 7 ++- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 51 +++++++++++++++------ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index baebb28e4f12..4d1c2c2cffd0 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -48,7 +48,7 @@ public class FrameHeader /// Additional statistics that guides the score processor to calculate the correct score for this frame. /// [Key(5)] - public Dictionary ScoreProcessorStatistics { get; set; } + public ScoreProcessorStatistics ScoreProcessorStatistics { get; set; } /// /// The time at which this frame was received by the server. @@ -71,13 +71,12 @@ public FrameHeader(ScoreInfo score, ScoreProcessor scoreProcessor) // copy for safety Statistics = new Dictionary(score.Statistics); - ScoreProcessorStatistics = new Dictionary(); - scoreProcessor.WriteScoreProcessorStatistics(ScoreProcessorStatistics); + ScoreProcessorStatistics = scoreProcessor.GetScoreProcessorStatistics(); } [JsonConstructor] [SerializationConstructor] - public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary statistics, Dictionary scoreProcessorStatistics, DateTimeOffset receivedTime) + public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary statistics, ScoreProcessorStatistics scoreProcessorStatistics, DateTimeOffset receivedTime) { TotalScore = totalScore; Accuracy = accuracy; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index f2b1607f5e43..013dd4b8e7a1 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using MessagePack; using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -397,29 +398,29 @@ public override void ResetFromReplayFrame(ReplayFrame frame) scoreResultCounts.Clear(); scoreResultCounts.AddRange(frame.Header.Statistics); - ReadScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics); + SetScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics); updateScore(); OnResetFromReplayFrame?.Invoke(); } - public virtual void WriteScoreProcessorStatistics(IDictionary statistics) + public ScoreProcessorStatistics GetScoreProcessorStatistics() => new ScoreProcessorStatistics { - statistics.Add(nameof(currentMaximumBaseScore), currentMaximumBaseScore); - statistics.Add(nameof(currentBaseScore), currentBaseScore); - statistics.Add(nameof(currentCountBasicJudgements), currentCountBasicJudgements); - statistics.Add(nameof(currentComboPortion), currentComboPortion); - statistics.Add(nameof(currentBonusPortion), currentBonusPortion); - } - - public virtual void ReadScoreProcessorStatistics(IReadOnlyDictionary statistics) + MaximumBaseScore = currentMaximumBaseScore, + BaseScore = currentBaseScore, + CountBasicJudgements = currentCountBasicJudgements, + ComboPortion = currentComboPortion, + BonusPortion = currentBonusPortion + }; + + public void SetScoreProcessorStatistics(ScoreProcessorStatistics statistics) { - currentMaximumBaseScore = (double)statistics.GetValueOrDefault(nameof(currentMaximumBaseScore), 0); - currentBaseScore = (double)statistics.GetValueOrDefault(nameof(currentBaseScore), 0); - currentCountBasicJudgements = (int)statistics.GetValueOrDefault(nameof(currentCountBasicJudgements), 0); - currentComboPortion = (double)statistics.GetValueOrDefault(nameof(currentComboPortion), 0); - currentBonusPortion = (double)statistics.GetValueOrDefault(nameof(currentBonusPortion), 0); + currentMaximumBaseScore = statistics.MaximumBaseScore; + currentBaseScore = statistics.BaseScore; + currentCountBasicJudgements = statistics.CountBasicJudgements; + currentComboPortion = statistics.ComboPortion; + currentBonusPortion = statistics.BonusPortion; } #region Static helper methods @@ -493,4 +494,24 @@ public enum ScoringMode [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.ClassicScoreDisplay))] Classic } + + [Serializable] + [MessagePackObject] + public class ScoreProcessorStatistics + { + [Key(0)] + public double MaximumBaseScore { get; set; } + + [Key(1)] + public double BaseScore { get; set; } + + [Key(2)] + public int CountBasicJudgements { get; set; } + + [Key(3)] + public double ComboPortion { get; set; } + + [Key(4)] + public double BonusPortion { get; set; } + } } From 62d504af921f7de8ac6c7933425aec975f6c3206 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 May 2023 15:59:24 +0900 Subject: [PATCH 25/41] Fix base implementation of ComputeTotalScore --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 013dd4b8e7a1..2108e99def81 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -297,13 +297,9 @@ private void updateScore() protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return - (int)Math.Round - (( - 700000 * comboProgress + - 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress + - bonusPortion - ) * scoreMultiplier); + return 700000 * comboProgress + + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress + + bonusPortion; } /// From f8101fbbc7b3606d6e8510846eb186f221bef85e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 May 2023 17:44:17 +0900 Subject: [PATCH 26/41] Rename variables --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 2108e99def81..c539820f1332 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -289,10 +289,10 @@ private void updateScore() { Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1; - double comboRatio = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; - double accuracyRatio = maximumCountBasicJudgements > 0 ? (double)currentCountBasicJudgements / maximumCountBasicJudgements : 1; + double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; + double accuracyProcess = maximumCountBasicJudgements > 0 ? (double)currentCountBasicJudgements / maximumCountBasicJudgements : 1; - TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboRatio, accuracyRatio, currentBonusPortion) * scoreMultiplier); + TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier); } protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) From 8570f825ed20a32733f104a2778df0685d92261e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 May 2023 17:47:35 +0900 Subject: [PATCH 27/41] Consider all accuracy judgements in accuracy progress --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 28 +++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index c539820f1332..8c3cd2701235 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -110,14 +110,14 @@ public partial class ScoreProcessor : JudgementProcessor private double currentBaseScore; /// - /// The count of all basic judgements in the beatmap. + /// The count of all accuracy-affecting judgements in the beatmap. /// - private int maximumCountBasicJudgements; + private int maximumCountAccuracyJudgements; /// - /// The count of basic judgements at the current point in time. + /// The count of accuracy-affecting judgements at the current point in time. /// - private int currentCountBasicJudgements; + private int currentCountAccuracyJudgements; /// /// The maximum combo score in the beatmap. @@ -207,13 +207,11 @@ protected sealed override void ApplyResultInternal(JudgementResult result) result.ComboAfterJudgement = Combo.Value; - if (result.Type.IsBasic()) - currentCountBasicJudgements++; - if (result.Type.AffectsAccuracy()) { currentMaximumBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult); currentBaseScore += Judgement.ToNumericResult(result.Type); + currentCountAccuracyJudgements++; } if (result.Type.IsBonus()) @@ -250,13 +248,11 @@ protected sealed override void RevertResultInternal(JudgementResult result) if (!result.Type.IsScorable()) return; - if (result.Type.IsBasic()) - currentCountBasicJudgements--; - if (result.Type.AffectsAccuracy()) { currentMaximumBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); currentBaseScore -= Judgement.ToNumericResult(result.Type); + currentCountAccuracyJudgements--; } if (result.Type.IsBonus()) @@ -290,7 +286,7 @@ private void updateScore() Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1; double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; - double accuracyProcess = maximumCountBasicJudgements > 0 ? (double)currentCountBasicJudgements / maximumCountBasicJudgements : 1; + double accuracyProcess = maximumCountAccuracyJudgements > 0 ? (double)currentCountAccuracyJudgements / maximumCountAccuracyJudgements : 1; TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier); } @@ -316,7 +312,7 @@ protected override void Reset(bool storeResults) if (storeResults) { maximumComboPortion = currentComboPortion; - maximumCountBasicJudgements = currentCountBasicJudgements; + maximumCountAccuracyJudgements = currentCountAccuracyJudgements; maximumResultCounts.Clear(); maximumResultCounts.AddRange(scoreResultCounts); @@ -328,7 +324,7 @@ protected override void Reset(bool storeResults) currentBaseScore = 0; currentMaximumBaseScore = 0; - currentCountBasicJudgements = 0; + currentCountAccuracyJudgements = 0; currentComboPortion = 0; currentBonusPortion = 0; @@ -405,7 +401,7 @@ public override void ResetFromReplayFrame(ReplayFrame frame) { MaximumBaseScore = currentMaximumBaseScore, BaseScore = currentBaseScore, - CountBasicJudgements = currentCountBasicJudgements, + CountAccuracyJudgements = currentCountAccuracyJudgements, ComboPortion = currentComboPortion, BonusPortion = currentBonusPortion }; @@ -414,7 +410,7 @@ public void SetScoreProcessorStatistics(ScoreProcessorStatistics statistics) { currentMaximumBaseScore = statistics.MaximumBaseScore; currentBaseScore = statistics.BaseScore; - currentCountBasicJudgements = statistics.CountBasicJudgements; + currentCountAccuracyJudgements = statistics.CountAccuracyJudgements; currentComboPortion = statistics.ComboPortion; currentBonusPortion = statistics.BonusPortion; } @@ -502,7 +498,7 @@ public class ScoreProcessorStatistics public double BaseScore { get; set; } [Key(2)] - public int CountBasicJudgements { get; set; } + public int CountAccuracyJudgements { get; set; } [Key(3)] public double ComboPortion { get; set; } From 3598ca91251ed4991d716c32878d751e8de13e60 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 May 2023 18:05:10 +0900 Subject: [PATCH 28/41] Adjust xmldoc --- osu.Game/Rulesets/Scoring/HitResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 83ed98768c63..0013a9f20d8e 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -150,7 +150,7 @@ public static bool BreaksCombo(this HitResult result) => AffectsCombo(result) && !IsHit(result); /// - /// Whether a increases/breaks the combo, and affects the combo portion of the score. + /// Whether a increases or breaks the combo. /// public static bool AffectsCombo(this HitResult result) { From d45b54399b376289531a37f5635f03dfa64cc8c2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 May 2023 18:15:32 +0900 Subject: [PATCH 29/41] Add back minimum/maximum accuracy --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 8c3cd2701235..b470c0985933 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -109,6 +109,11 @@ public partial class ScoreProcessor : JudgementProcessor /// private double currentBaseScore; + /// + /// The maximum sum of all accuracy-affecting judgements in the beatmap. + /// + private double maximumBaseScore; + /// /// The count of all accuracy-affecting judgements in the beatmap. /// @@ -284,6 +289,8 @@ protected virtual void RemoveScoreChange(JudgementResult result) private void updateScore() { Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1; + MinimumAccuracy.Value = maximumBaseScore > 0 ? currentBaseScore / maximumBaseScore : 0; + MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1; double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; double accuracyProcess = maximumCountAccuracyJudgements > 0 ? (double)currentCountAccuracyJudgements / maximumCountAccuracyJudgements : 1; @@ -311,6 +318,8 @@ protected override void Reset(bool storeResults) if (storeResults) { + maximumBaseScore = currentBaseScore; + maximumComboPortion = currentComboPortion; maximumCountAccuracyJudgements = currentCountAccuracyJudgements; @@ -334,9 +343,6 @@ protected override void Reset(bool storeResults) Rank.Disabled = false; Rank.Value = ScoreRank.X; HighestCombo.Value = 0; - - currentBaseScore = 0; - currentMaximumBaseScore = 0; } /// From 844c023fb71fc6b87386650e820c621b6ca5b2bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 May 2023 18:18:27 +0900 Subject: [PATCH 30/41] Fix tests --- .../Gameplay/TestSceneScoreProcessor.cs | 20 +- .../Rulesets/Scoring/ScoreProcessorTest.cs | 189 +++++------------- ...MultiplayerGameplayLeaderboardTestScene.cs | 14 +- 3 files changed, 75 insertions(+), 148 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index 90c768844397..fbe4dba8eddb 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -76,22 +76,38 @@ public void TestResetFromReplayFrame() // Reset with a miss instead. scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame { - Header = new FrameHeader(0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, DateTimeOffset.Now) + Header = new FrameHeader(0, 0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, new ScoreProcessorStatistics + { + MaximumBaseScore = 300, + BaseScore = 0, + CountAccuracyJudgements = 1, + ComboPortion = 0, + BonusPortion = 0 + }, DateTimeOffset.Now) }); Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); + Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(0)); // Reset with no judged hit. scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame { - Header = new FrameHeader(0, 0, 0, new Dictionary(), DateTimeOffset.Now) + Header = new FrameHeader(0, 0, 0, 0, new Dictionary(), new ScoreProcessorStatistics + { + MaximumBaseScore = 0, + BaseScore = 0, + CountAccuracyJudgements = 0, + ComboPortion = 0, + BonusPortion = 0 + }, DateTimeOffset.Now) }); Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); Assert.That(scoreProcessor.JudgedHits, Is.Zero); Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); + Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1)); } [Test] diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 826c610f568f..f51a4ad52b04 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -14,11 +14,12 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; 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.Scoring.Legacy; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Rulesets.Scoring @@ -31,7 +32,7 @@ public partial class ScoreProcessorTest [SetUp] public void SetUp() { - scoreProcessor = new ScoreProcessor(new TestRuleset()); + scoreProcessor = new ScoreProcessor(new OsuRuleset()); beatmap = new TestBeatmap(new RulesetInfo()) { HitObjects = new List @@ -41,15 +42,14 @@ public void SetUp() }; } - [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)] - [TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)] + [TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)] + [TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)] [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] - [TestCase(ScoringMode.Classic, HitResult.Meh, 20)] - [TestCase(ScoringMode.Classic, HitResult.Ok, 23)] + [TestCase(ScoringMode.Classic, HitResult.Meh, 0)] + [TestCase(ScoringMode.Classic, HitResult.Ok, 2)] [TestCase(ScoringMode.Classic, HitResult.Great, 36)] public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { - scoreProcessor.Mode.Value = scoringMode; scoreProcessor.ApplyBeatmap(beatmap); var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement()) @@ -58,7 +58,7 @@ public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int e }; scoreProcessor.ApplyResult(judgementResult); - Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d)); + Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d)); } /// @@ -70,39 +70,29 @@ public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int e /// Expected score after all objects have been judged, rounded to the nearest integer. /// /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo. - /// - /// For standardised scoring, is calculated using the following formula: - /// 1_000_000 * (((3 * ) / (4 * )) * 30% + (bestCombo / maxCombo) * 70%) - /// - /// - /// For classic scoring, is calculated using the following formula: - /// / * 936 - /// where 936 is simplified from: - /// 75% * 4 * 300 * (1 + 1/25) - /// /// - [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 492_857)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0) - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0) - [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points) - [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points) + [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] + [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)] + [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)] + [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)] + [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)] + [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 541_894)] + [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] + [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)] + [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] + [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] - [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 86)] - [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 104)] - [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 140)] - [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 190)] - [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 190)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 18)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 31)] + [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 4)] + [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)] + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)] + [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)] + [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 12)] + [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 9)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)] [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)] public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) @@ -113,59 +103,18 @@ public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hit { HitObjects = new List(Enumerable.Repeat(new TestHitObject(maxResult), 4)) }; - scoreProcessor.Mode.Value = scoringMode; scoreProcessor.ApplyBeatmap(fourObjectBeatmap); for (int i = 0; i < 4; i++) { - var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement()) + var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult)) { Type = i == 2 ? minResult : hitResult }; scoreProcessor.ApplyResult(judgementResult); } - Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d)); - } - - /// - /// This test uses a beatmap with four small ticks and one object with the of . - /// Its goal is to ensure that with the of , - /// small ticks contribute to the accuracy portion, but not the combo portion. - /// In contrast, does not have separate combo and accuracy portion (they are multiplied by each other). - /// - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 - [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 34)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 30)] - public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore) - { - IEnumerable hitObjects = Enumerable - .Repeat(new TestHitObject(HitResult.SmallTickHit), 4) - .Append(new TestHitObject(HitResult.Ok)); - IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo()) - { - HitObjects = hitObjects.ToList() - }; - scoreProcessor.Mode.Value = scoringMode; - scoreProcessor.ApplyBeatmap(fiveObjectBeatmap); - - for (int i = 0; i < 4; i++) - { - var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement()) - { - Type = i == 2 ? HitResult.SmallTickMiss : hitResult - }; - scoreProcessor.ApplyResult(judgementResult); - } - - var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement()) - { - Type = HitResult.Ok - }; - scoreProcessor.ApplyResult(lastJudgementResult); - - Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d)); + Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d)); } [Test] @@ -173,10 +122,9 @@ public void TestEmptyBeatmap( [Values(ScoringMode.Standardised, ScoringMode.Classic)] ScoringMode scoringMode) { - scoreProcessor.Mode.Value = scoringMode; scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo())); - Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); + Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.Zero); } [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)] @@ -294,28 +242,6 @@ public void TestIsScorable(HitResult hitResult, bool expectedReturnValue) Assert.AreEqual(expectedReturnValue, hitResult.IsScorable()); } - [TestCase(HitResult.Perfect, 1_000_000)] - [TestCase(HitResult.SmallTickHit, 1_000_000)] - [TestCase(HitResult.LargeTickHit, 1_000_000)] - [TestCase(HitResult.SmallBonus, 1_000_000 + Judgement.SMALL_BONUS_SCORE)] - [TestCase(HitResult.LargeBonus, 1_000_000 + Judgement.LARGE_BONUS_SCORE)] - public void TestGetScoreWithExternalStatistics(HitResult result, int expectedScore) - { - var statistic = new Dictionary { { result, 1 } }; - - scoreProcessor.ApplyBeatmap(new Beatmap - { - HitObjects = { new TestHitObject(result) } - }); - - Assert.That(scoreProcessor.ComputeScore(ScoringMode.Standardised, new ScoreInfo - { - Ruleset = new TestRuleset().RulesetInfo, - MaxCombo = result.AffectsCombo() ? 1 : 0, - Statistics = statistic - }), Is.EqualTo(expectedScore).Within(0.5d)); - } - #pragma warning disable CS0618 [Test] public void TestLegacyComboIncrease() @@ -330,29 +256,6 @@ public void TestLegacyComboIncrease() Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True); Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True); Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease)); - - // Cannot be used to apply results. - Assert.Throws(() => scoreProcessor.ApplyBeatmap(new Beatmap - { - HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) } - })); - - ScoreInfo testScore = new ScoreInfo - { - MaxCombo = 1, - Statistics = new Dictionary - { - { HitResult.Great, 1 } - }, - MaximumStatistics = new Dictionary - { - { HitResult.Great, 1 }, - { HitResult.LegacyComboIncrease, 1 } - } - }; - - double totalScore = new TestScoreProcessor().ComputeScore(ScoringMode.Standardised, testScore); - Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%). } #pragma warning restore CS0618 @@ -362,16 +265,24 @@ public void TestAccuracyWhenNearPerfect() const int count_judgements = 1000; const int count_misses = 1; - double actual = new TestScoreProcessor().ComputeAccuracy(new ScoreInfo + beatmap = new TestBeatmap(new RulesetInfo()) { - Statistics = new Dictionary + HitObjects = new List(Enumerable.Repeat(new TestHitObject(HitResult.Great), count_judgements)) + }; + + scoreProcessor = new TestScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + + for (int i = 0; i < beatmap.HitObjects.Count; i++) + { + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great)) { - { HitResult.Great, count_judgements - count_misses }, - { HitResult.Miss, count_misses } - } - }); + Type = i == 0 ? HitResult.Miss : HitResult.Great + }); + } const double expected = (count_judgements - count_misses) / (double)count_judgements; + double actual = scoreProcessor.Accuracy.Value; Assert.That(actual, Is.Not.EqualTo(0.0)); Assert.That(actual, Is.Not.EqualTo(1.0)); @@ -419,14 +330,18 @@ public TestHitObject(HitResult maxResult) private partial class TestScoreProcessor : ScoreProcessor { - protected override double DefaultAccuracyPortion => 0.5; - protected override double DefaultComboPortion => 0.5; - public TestScoreProcessor() : base(new TestRuleset()) { } + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) + { + return 500000 * comboProgress + + 500000 * Accuracy.Value * accuracyProgress + + bonusPortion; + } + // ReSharper disable once MemberHidesStaticFromOuterClass private class TestRuleset : Ruleset { diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 649c662e41a3..906eea95536a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -21,7 +21,6 @@ using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Multiplayer @@ -188,15 +187,12 @@ protected void UpdateUserStatesRandomly() if (!lastHeaders.TryGetValue(userId, out var header)) { - lastHeaders[userId] = header = new FrameHeader(new ScoreInfo + lastHeaders[userId] = header = new FrameHeader(0, 0, 0, 0, new Dictionary { - Statistics = new Dictionary - { - [HitResult.Miss] = 0, - [HitResult.Meh] = 0, - [HitResult.Great] = 0 - } - }); + [HitResult.Miss] = 0, + [HitResult.Meh] = 0, + [HitResult.Great] = 0 + }, new ScoreProcessorStatistics(), DateTimeOffset.Now); } switch (RNG.Next(0, 3)) From 7658536b5afcd3b499aa0b5b5bc70ba5b22b88ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 May 2023 19:32:19 +0900 Subject: [PATCH 31/41] Fix CI issues --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 14 -------------- osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ---- 2 files changed, 18 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index f51a4ad52b04..e5e96d20333f 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -289,20 +289,6 @@ public void TestAccuracyWhenNearPerfect() Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON)); } - private class TestRuleset : Ruleset - { - public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); - - public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException(); - - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); - - public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException(); - - public override string Description => string.Empty; - public override string ShortName => string.Empty; - } - private class TestJudgement : Judgement { public override HitResult MaxResult { get; } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 1f93389e94ea..b75f3d86ff46 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -67,9 +66,6 @@ public partial class ScorePanelList : CompositeDrawable public readonly Bindable SelectedScore = new Bindable(); - [Resolved] - private ScoreManager scoreManager { get; set; } - private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); private readonly Flow flow; private readonly Scroll scroll; From 6d9ba9248dd2dd01a75c988fceabc44fd716f2ad Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 May 2023 16:38:22 +0900 Subject: [PATCH 32/41] Massage tests a bit more --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 4 ++-- .../Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs | 4 ++-- .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 4 ++-- .../Visual/Gameplay/TestSceneSkinnableScoreCounter.cs | 5 +++-- .../Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs | 5 +++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index ae46dda75082..f97019e466ce 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -31,8 +31,8 @@ public partial class TestSceneHUDOverlay : OsuManualInputManagerTestScene private HUDOverlay hudOverlay = null!; - [Cached] - private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + [Cached(typeof(ScoreProcessor))] + private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor; [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 93fec60de4b7..4ae115a68dfa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene { - [Cached] - private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + [Cached(typeof(ScoreProcessor))] + private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor; [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 0439656aae3a..89432940ba41 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -28,8 +28,8 @@ public partial class TestSceneSkinnableHUDOverlay : SkinnableTestScene { private HUDOverlay hudOverlay; - [Cached] - private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + [Cached(typeof(ScoreProcessor))] + private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor; [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs index c95e8ee5b2ec..2cb3303dd69c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs @@ -10,13 +10,14 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osu.Game.Tests.Gameplay; namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene { - [Cached] - private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + [Cached(typeof(ScoreProcessor))] + private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor; protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index 8ae6a2a5fcb8..dbd14db818b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -16,13 +16,14 @@ using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; +using osu.Game.Tests.Gameplay; namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene { - [Cached] - private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + [Cached(typeof(ScoreProcessor))] + private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor; private readonly BindableList scores = new BindableList(); From 57c63dbb290a32faa39e9a68af10dc032bfeeb23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 May 2023 19:24:15 +0900 Subject: [PATCH 33/41] Add xmldoc for `GetDisplayScore` --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 74a39254355d..b7247ed8ccd4 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -57,6 +57,10 @@ public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardS public BindableInt Combo { get; } = new BindableInt(); public BindableBool HasQuit { get; } = new BindableBool(); public Bindable DisplayOrder { get; } = new Bindable(); + + /// + /// A function providing a display score. If a custom function is not provided, this defaults to using . + /// public Func GetDisplayScore { get; set; } public Color4? BackgroundColour { get; set; } From a789d1e49c85687e120e42f83a7dbc4a6980c0eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 May 2023 18:38:16 +0900 Subject: [PATCH 34/41] Add xmldoc and change naming around `ScoreProcessorStatistics` a bit --- .../Gameplay/TestSceneScoreProcessor.cs | 4 +-- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 32 ++++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index fbe4dba8eddb..a261185473fc 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -80,7 +80,7 @@ public void TestResetFromReplayFrame() { MaximumBaseScore = 300, BaseScore = 0, - CountAccuracyJudgements = 1, + AccuracyJudgementCount = 1, ComboPortion = 0, BonusPortion = 0 }, DateTimeOffset.Now) @@ -98,7 +98,7 @@ public void TestResetFromReplayFrame() { MaximumBaseScore = 0, BaseScore = 0, - CountAccuracyJudgements = 0, + AccuracyJudgementCount = 0, ComboPortion = 0, BonusPortion = 0 }, DateTimeOffset.Now) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b470c0985933..a0d8187642f5 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -407,7 +407,7 @@ public override void ResetFromReplayFrame(ReplayFrame frame) { MaximumBaseScore = currentMaximumBaseScore, BaseScore = currentBaseScore, - CountAccuracyJudgements = currentCountAccuracyJudgements, + AccuracyJudgementCount = currentCountAccuracyJudgements, ComboPortion = currentComboPortion, BonusPortion = currentBonusPortion }; @@ -416,7 +416,7 @@ public void SetScoreProcessorStatistics(ScoreProcessorStatistics statistics) { currentMaximumBaseScore = statistics.MaximumBaseScore; currentBaseScore = statistics.BaseScore; - currentCountAccuracyJudgements = statistics.CountAccuracyJudgements; + currentCountAccuracyJudgements = statistics.AccuracyJudgementCount; currentComboPortion = statistics.ComboPortion; currentBonusPortion = statistics.BonusPortion; } @@ -497,18 +497,40 @@ public enum ScoringMode [MessagePackObject] public class ScoreProcessorStatistics { + /// + /// The sum of all accuracy-affecting judgements at the current point in time. + /// + /// + /// Used to compute accuracy. + /// See: and . + /// [Key(0)] - public double MaximumBaseScore { get; set; } + public double BaseScore { get; set; } + /// + /// The maximum sum of accuracy-affecting judgements at the current point in time. + /// + /// + /// Used to compute accuracy. + /// [Key(1)] - public double BaseScore { get; set; } + public double MaximumBaseScore { get; set; } + /// + /// The count of accuracy-affecting judgements at the current point in time. + /// [Key(2)] - public int CountAccuracyJudgements { get; set; } + public int AccuracyJudgementCount { get; set; } + /// + /// The combo score at the current point in time. + /// [Key(3)] public double ComboPortion { get; set; } + /// + /// The bonus score at the current point in time. + /// [Key(4)] public double BonusPortion { get; set; } } From 22be045de3bd788529e1dd52eabf41597350abd3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 May 2023 18:48:17 +0900 Subject: [PATCH 35/41] Apply NRT to `GameplayScoreCounter` --- osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs index a696d2cad7d2..bc953e05d20d 100644 --- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -15,8 +13,9 @@ namespace osu.Game.Screens.Play.HUD { public abstract partial class GameplayScoreCounter : ScoreCounter { - private Bindable scoreDisplayMode; - private Bindable totalScoreBindable; + private Bindable scoreDisplayMode = null!; + + private Bindable totalScoreBindable = null!; protected GameplayScoreCounter() : base(6) From 9a886125ad973f7af619c7f284e340e26855ec79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 May 2023 19:00:01 +0900 Subject: [PATCH 36/41] Ensure `GameplayScoreCounter`'s display score is updated on `ScoringMode` change This isn't strictly required, but only because of a kind of hacky behaviour where `HUDOverlay` will recreate all components on a scoring mode change currently (see https://github.com/ppy/osu/blob/8f6df5ea0f7f721c630fc8cad93bb3eef869d1d9/osu.Game/Screens/Play/HUDOverlay.cs#L410-L418). Best we do this just in case that happens to go away in the future. --- osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs index bc953e05d20d..a086aa6d7291 100644 --- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs @@ -25,6 +25,9 @@ protected GameplayScoreCounter() [BackgroundDependencyLoader] private void load(OsuConfigManager config, ScoreProcessor scoreProcessor) { + totalScoreBindable = scoreProcessor.TotalScore.GetBoundCopy(); + totalScoreBindable.BindValueChanged(_ => updateDisplayScore()); + scoreDisplayMode = config.GetBindable(OsuSetting.ScoreDisplayMode); scoreDisplayMode.BindValueChanged(scoreMode => { @@ -41,10 +44,11 @@ private void load(OsuConfigManager config, ScoreProcessor scoreProcessor) default: throw new ArgumentOutOfRangeException(nameof(scoreMode)); } + + updateDisplayScore(); }, true); - totalScoreBindable = scoreProcessor.TotalScore.GetBoundCopy(); - totalScoreBindable.BindValueChanged(_ => Current.Value = scoreProcessor.GetDisplayScore(scoreDisplayMode.Value), true); + void updateDisplayScore() => Current.Value = scoreProcessor.GetDisplayScore(scoreDisplayMode.Value); } } } From fcd7a1d51a915d289e14a69d6b29a8294f59bfb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 May 2023 19:41:53 +0900 Subject: [PATCH 37/41] Move `GetDisplayScore` xmldoc to interface and remove getter --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 3 --- osu.Game/Screens/Play/HUD/ILeaderboardScore.cs | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index b7247ed8ccd4..496fc8201966 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -58,9 +58,6 @@ public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardS public BindableBool HasQuit { get; } = new BindableBool(); public Bindable DisplayOrder { get; } = new Bindable(); - /// - /// A function providing a display score. If a custom function is not provided, this defaults to using . - /// public Func GetDisplayScore { get; set; } public Color4? BackgroundColour { get; set; } diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs index cc1d83e0c708..5de38396fb54 100644 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -23,6 +23,9 @@ public interface ILeaderboardScore /// Bindable DisplayOrder { get; } - Func GetDisplayScore { get; set; } + /// + /// A function providing a display score. If a custom function is not provided, this defaults to using . + /// + Func GetDisplayScore { set; } } } From 1a6d9e9ff0dded325167a333eed03f1e8e85f928 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 May 2023 19:46:50 +0900 Subject: [PATCH 38/41] Apply NRT to `GameplayLeaderboardScore` and change `GetDisplayedScore` handling I don't feel too confident with the default scoring function being assigned in the constructor to a publicly settable delegate. This just feels a bit more elegant, and handles the (likely-never-used) case where we need to restore the default function. An alternative would be to provide the function as a `ctor` argument, but I believe that wasn't done here to allow using the `ILeaderboardScore` interface. --- .../Play/HUD/GameplayLeaderboardScore.cs | 38 +++++++++---------- .../Screens/Play/HUD/ILeaderboardScore.cs | 6 +-- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 496fc8201966..4ac2f1afdae5 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -50,7 +47,7 @@ public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardS public Bindable Expanded = new Bindable(); - private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; + private OsuSpriteText positionText = null!, scoreText = null!, accuracyText = null!, comboText = null!, usernameText = null!; public BindableLong TotalScore { get; } = new BindableLong(); public BindableDouble Accuracy { get; } = new BindableDouble(1); @@ -58,7 +55,12 @@ public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardS public BindableBool HasQuit { get; } = new BindableBool(); public Bindable DisplayOrder { get; } = new Bindable(); - public Func GetDisplayScore { get; set; } + private Func? getDisplayScoreFunction; + + public Func GetDisplayScore + { + set => getDisplayScoreFunction = value; + } public Color4? BackgroundColour { get; set; } @@ -86,32 +88,31 @@ public int? ScorePosition } } - [CanBeNull] - public IUser User { get; } + public IUser? User { get; } /// /// Whether this score is the local user or a replay player (and should be focused / always visible). /// public readonly bool Tracked; - private Container mainFillContainer; + private Container mainFillContainer = null!; - private Box centralFill; + private Box centralFill = null!; - private Container backgroundPaddingAdjustContainer; + private Container backgroundPaddingAdjustContainer = null!; - private GridContainer gridContainer; + private GridContainer gridContainer = null!; - private Container scoreComponents; + private Container scoreComponents = null!; - private IBindable scoreDisplayMode; + private IBindable scoreDisplayMode = null!; /// /// Creates a new . /// /// The score's player. /// Whether the player is the local user or a replay player. - public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked) + public GameplayLeaderboardScore(IUser? user, bool tracked) { User = user; Tracked = tracked; @@ -242,7 +243,7 @@ private void load(OsuColour colours, OsuConfigManager osuConfigManager) Origin = Anchor.CentreLeft, Colour = Color4.White, Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), - Text = User?.Username, + Text = User?.Username ?? string.Empty, Truncate = true, Shadow = false, } @@ -313,11 +314,6 @@ private void load(OsuColour colours, OsuConfigManager osuConfigManager) HasQuit.BindValueChanged(_ => updateState()); } - private void updateScore() - { - scoreText.Text = GetDisplayScore(scoreDisplayMode.Value).ToString("N0"); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -328,6 +324,8 @@ protected override void LoadComplete() FinishTransforms(true); } + private void updateScore() => scoreText.Text = (getDisplayScoreFunction?.Invoke(scoreDisplayMode.Value) ?? TotalScore.Value).ToString("N0"); + private void changeExpandedState(ValueChangedEvent expanded) { if (expanded.NewValue) diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs index 5de38396fb54..1a5d7fd9a8ec 100644 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Bindables; using osu.Game.Rulesets.Scoring; @@ -24,8 +22,10 @@ public interface ILeaderboardScore Bindable DisplayOrder { get; } /// - /// A function providing a display score. If a custom function is not provided, this defaults to using . + /// A custom function which handles converting a score to a display score using a provide . /// + /// + /// If no function is provided, will be used verbatim. Func GetDisplayScore { set; } } } From df662afbd56a9f2e7042eb4f1900ac6a1647c5f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 May 2023 20:00:42 +0900 Subject: [PATCH 39/41] Pass `ScoreProcessorStatistics` to `FrameHeader`, rather than the full processor --- osu.Game/Online/Spectator/FrameDataBundle.cs | 2 +- osu.Game/Online/Spectator/FrameHeader.cs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index b93684743432..d58ddd531098 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -24,7 +24,7 @@ public class FrameDataBundle public FrameDataBundle(ScoreInfo score, ScoreProcessor scoreProcessor, IList frames) { Frames = frames; - Header = new FrameHeader(score, scoreProcessor); + Header = new FrameHeader(score, scoreProcessor.GetScoreProcessorStatistics()); } [JsonConstructor] diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index 4d1c2c2cffd0..45f920e65b96 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -60,18 +60,17 @@ public class FrameHeader /// Construct header summary information from a point-in-time reference to a score which is actively being played. /// /// The score for reference. - /// The score processor for reference. - public FrameHeader(ScoreInfo score, ScoreProcessor scoreProcessor) + /// The score processor statistics for the current point in time. + public FrameHeader(ScoreInfo score, ScoreProcessorStatistics statistics) { TotalScore = score.TotalScore; Accuracy = score.Accuracy; Combo = score.Combo; MaxCombo = score.MaxCombo; - // copy for safety Statistics = new Dictionary(score.Statistics); - ScoreProcessorStatistics = scoreProcessor.GetScoreProcessorStatistics(); + ScoreProcessorStatistics = statistics; } [JsonConstructor] From b3ca409339fd15fa7f06608b584f614847d58885 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 May 2023 20:08:22 +0900 Subject: [PATCH 40/41] Rename a few remaining `CountAccuracyJudgement` variable --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index a0d8187642f5..ac17de32d878 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -117,12 +117,12 @@ public partial class ScoreProcessor : JudgementProcessor /// /// The count of all accuracy-affecting judgements in the beatmap. /// - private int maximumCountAccuracyJudgements; + private int maximumAccuracyJudgementCount; /// /// The count of accuracy-affecting judgements at the current point in time. /// - private int currentCountAccuracyJudgements; + private int currentAccuracyJudgementCount; /// /// The maximum combo score in the beatmap. @@ -216,7 +216,7 @@ protected sealed override void ApplyResultInternal(JudgementResult result) { currentMaximumBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult); currentBaseScore += Judgement.ToNumericResult(result.Type); - currentCountAccuracyJudgements++; + currentAccuracyJudgementCount++; } if (result.Type.IsBonus()) @@ -257,7 +257,7 @@ protected sealed override void RevertResultInternal(JudgementResult result) { currentMaximumBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); currentBaseScore -= Judgement.ToNumericResult(result.Type); - currentCountAccuracyJudgements--; + currentAccuracyJudgementCount--; } if (result.Type.IsBonus()) @@ -293,7 +293,7 @@ private void updateScore() MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1; double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; - double accuracyProcess = maximumCountAccuracyJudgements > 0 ? (double)currentCountAccuracyJudgements / maximumCountAccuracyJudgements : 1; + double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1; TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier); } @@ -321,7 +321,7 @@ protected override void Reset(bool storeResults) maximumBaseScore = currentBaseScore; maximumComboPortion = currentComboPortion; - maximumCountAccuracyJudgements = currentCountAccuracyJudgements; + maximumAccuracyJudgementCount = currentAccuracyJudgementCount; maximumResultCounts.Clear(); maximumResultCounts.AddRange(scoreResultCounts); @@ -333,7 +333,7 @@ protected override void Reset(bool storeResults) currentBaseScore = 0; currentMaximumBaseScore = 0; - currentCountAccuracyJudgements = 0; + currentAccuracyJudgementCount = 0; currentComboPortion = 0; currentBonusPortion = 0; @@ -407,7 +407,7 @@ public override void ResetFromReplayFrame(ReplayFrame frame) { MaximumBaseScore = currentMaximumBaseScore, BaseScore = currentBaseScore, - AccuracyJudgementCount = currentCountAccuracyJudgements, + AccuracyJudgementCount = currentAccuracyJudgementCount, ComboPortion = currentComboPortion, BonusPortion = currentBonusPortion }; @@ -416,7 +416,7 @@ public void SetScoreProcessorStatistics(ScoreProcessorStatistics statistics) { currentMaximumBaseScore = statistics.MaximumBaseScore; currentBaseScore = statistics.BaseScore; - currentCountAccuracyJudgements = statistics.AccuracyJudgementCount; + currentAccuracyJudgementCount = statistics.AccuracyJudgementCount; currentComboPortion = statistics.ComboPortion; currentBonusPortion = statistics.BonusPortion; } From 4e0f40bee5f31dc15b6165d2d65ef7a283048ffc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 May 2023 14:20:26 +0900 Subject: [PATCH 41/41] Split out multiplier retrieval into a function and use a default multiplier for all rulesets --- .../Scoring/Legacy/ScoreInfoExtensions.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index af991c7ea337..84bf6d15f6d9 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -23,10 +23,32 @@ private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, if (mode == ScoringMode.Standardised) return score; + int maxBasicJudgements = maximumStatistics + .Where(k => k.Key.IsBasic()) + .Select(k => k.Value) + .DefaultIfEmpty(0) + .Sum(); + + // 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 scaledRawScore = score / ScoreProcessor.MAX_SCORE; + + return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * getStandardisedToClassicMultiplier(rulesetId)); + } + + /// + /// Returns a ballpark multiplier which gives a similar "feel" for how large scores should get when displayed in "classic" mode. + /// This is different per ruleset to match the different algorithms used in the scoring implementation. + /// + private static double getStandardisedToClassicMultiplier(int rulesetId) + { double multiplier; switch (rulesetId) { + // For non-legacy rulesets, just go with the same as the osu! ruleset. + // This is arbitrary, but at least allows the setting to do something to the score. + default: case 0: multiplier = 36; break; @@ -42,17 +64,9 @@ private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, case 3: multiplier = 16; break; - - default: - return score; } - int maxBasicJudgements = maximumStatistics.Where(k => k.Key.IsBasic()).Select(k => k.Value).DefaultIfEmpty(0).Sum(); - - // 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 scaledRawScore = score / ScoreProcessor.MAX_SCORE; - return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * multiplier); + return multiplier; } public static int? GetCountGeki(this ScoreInfo scoreInfo)