From 02111e38542edaa0657c9e1ea7ac48ed4b0d10f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 1 Jun 2023 13:22:37 +0900 Subject: [PATCH 01/40] Implement ScoreV1 calculation for OsuRuleset --- .../Difficulty/OsuDifficultyCalculator.cs | 149 +++++++++++++++++- .../Difficulty/DifficultyAttributes.cs | 18 ++- 2 files changed, 162 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1e83d6d820..0e06e1e28f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -11,6 +11,8 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; @@ -26,9 +28,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty public override int Version => 20220902; + private readonly IWorkingBeatmap workingBeatmap; + public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -71,7 +76,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 ); - double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; + double starRating = basePerformance > 0.00001 + ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) + : 0; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; @@ -90,6 +97,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { StarRating = starRating, Mods = mods, + TotalScoreV1 = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, AimDifficulty = aimRating, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, @@ -142,4 +150,143 @@ namespace osu.Game.Rulesets.Osu.Difficulty new MultiMod(new OsuModFlashlight(), new OsuModHidden()) }; } + + public abstract class ScoreV1Processor + { + protected readonly int DifficultyPeppyStars; + protected readonly double ScoreMultiplier; + + protected readonly IBeatmap PlayableBeatmap; + + protected ScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + PlayableBeatmap = playableBeatmap; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + DifficultyPeppyStars = (int)Math.Round( + (playableBeatmap.Difficulty.DrainRate + + playableBeatmap.Difficulty.OverallDifficulty + + playableBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / playableBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + ScoreMultiplier = 1 * DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + } + } + + public class OsuScoreV1Processor : ScoreV1Processor + { + public int TotalScore { get; private set; } + private int combo; + + public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + : base(baseBeatmap, playableBeatmap, mods) + { + foreach (var obj in playableBeatmap.HitObjects) + increaseScore(obj); + } + + private void increaseScore(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + int scoreIncrease = 0; + + switch (hitObject) + { + case SliderHeadCircle: + case SliderTailCircle: + case SliderRepeat: + scoreIncrease = 30; + break; + + case SliderTick: + scoreIncrease = 10; + break; + + case SpinnerBonusTick: + scoreIncrease = 1100; + increaseCombo = false; + break; + + case SpinnerTick: + scoreIncrease = 100; + increaseCombo = false; + break; + + case HitCircle: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case Slider: + foreach (var nested in hitObject.NestedHitObjects) + increaseScore(nested); + + scoreIncrease = 300; + increaseCombo = false; + addScoreComboMultiplier = true; + break; + + case Spinner spinner: + // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. + // We'll redo the calculations to match osu-stable here... + const double maximum_rotations_per_second = 477.0 / 60; + double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(PlayableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + double secondsDuration = spinner.Duration / 1000; + + // The total amount of half spins possible for the entire spinner. + int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); + // To be able to receive bonus points, the spinner must be rotated another 1.5 times. + int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; + + for (int i = 0; i <= totalHalfSpinsPossible; i++) + { + if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) + increaseScore(new SpinnerBonusTick()); + else if (i > 1 && i % 2 == 0) + increaseScore(new SpinnerTick()); + } + + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + scoreIncrease += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); + } + + if (increaseCombo) + combo++; + + TotalScore += scoreIncrease; + } + } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index bd45482235..d0fbd0afaf 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -27,6 +26,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; + protected const int ATTRIB_ID_TOTAL_SCORE_V1 = 23; /// /// The mods which were applied to the beatmap. @@ -36,15 +36,21 @@ namespace osu.Game.Rulesets.Difficulty /// /// The combined star rating of all skills. /// - [JsonProperty("star_rating", Order = -3)] + [JsonProperty("star_rating", Order = -4)] public double StarRating { get; set; } /// /// The maximum achievable combo. /// - [JsonProperty("max_combo", Order = -2)] + [JsonProperty("max_combo", Order = -3)] public int MaxCombo { get; set; } + /// + /// The total score achievable in ScoreV1. + /// + [JsonProperty("total_score_v1", Order = -2)] + public int TotalScoreV1 { get; set; } + /// /// Creates new . /// @@ -69,7 +75,10 @@ namespace osu.Game.Rulesets.Difficulty /// /// See: osu_difficulty_attribs table. /// - public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() => Enumerable.Empty<(int, object)>(); + public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() + { + yield return (ATTRIB_ID_TOTAL_SCORE_V1, TotalScoreV1); + } /// /// Reads osu-web database attribute mappings into this object. @@ -78,6 +87,7 @@ namespace osu.Game.Rulesets.Difficulty /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc). public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { + TotalScoreV1 = (int)values[ATTRIB_ID_TOTAL_SCORE_V1]; } } } From e402c6d2b4275b87468d1280826ae1c96d338c46 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Jun 2023 21:53:25 +0900 Subject: [PATCH 02/40] Write max combo attribute from base class --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs | 2 -- osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs | 2 -- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 2 -- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs | 2 -- osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 2 ++ 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 2d01153f98..5c64643fd4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Todo: osu!catch should not output star rating in the 'aim' attribute. yield return (ATTRIB_ID_AIM, StarRating); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty StarRating = values[ATTRIB_ID_AIM]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index d259c2af8e..db60e757e1 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty foreach (var v in base.ToDatabaseAttributes()) yield return v; - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } @@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { base.FromDatabaseAttributes(values, onlineInfo); - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 03540abddb..24d5635104 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED, SpeedDifficulty); yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); if (ShouldSerializeFlashlightRating()) @@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficulty = values[ATTRIB_ID_SPEED]; OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 72452e27b3..1664c941f8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty foreach (var v in base.ToDatabaseAttributes()) yield return v; - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } @@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { base.FromDatabaseAttributes(values, onlineInfo); - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index d0fbd0afaf..ee02376939 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { + yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_TOTAL_SCORE_V1, TotalScoreV1); } @@ -87,6 +88,7 @@ namespace osu.Game.Rulesets.Difficulty /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc). public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { + MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; TotalScoreV1 = (int)values[ATTRIB_ID_TOTAL_SCORE_V1]; } } From 77c745cc94d26d5998ba121c4b5fbe137a1af09c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Jun 2023 17:25:28 +0900 Subject: [PATCH 03/40] "TotalScoreV1" -> "LegacyTotalScore" --- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 0e06e1e28f..1011066892 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { StarRating = starRating, Mods = mods, - TotalScoreV1 = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, + LegacyTotalScore = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, AimDifficulty = aimRating, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index ee02376939..8e30050a7f 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; - protected const int ATTRIB_ID_TOTAL_SCORE_V1 = 23; + protected const int ATTRIB_ID_LEGACY_TOTAL_SCORE = 23; /// /// The mods which were applied to the beatmap. @@ -46,10 +46,10 @@ namespace osu.Game.Rulesets.Difficulty public int MaxCombo { get; set; } /// - /// The total score achievable in ScoreV1. + /// The maximum achievable legacy total score. /// - [JsonProperty("total_score_v1", Order = -2)] - public int TotalScoreV1 { get; set; } + [JsonProperty("legacy_total_score", Order = -2)] + public int LegacyTotalScore { get; set; } /// /// Creates new . @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Difficulty public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); - yield return (ATTRIB_ID_TOTAL_SCORE_V1, TotalScoreV1); + yield return (ATTRIB_ID_LEGACY_TOTAL_SCORE, LegacyTotalScore); } /// @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Difficulty public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; - TotalScoreV1 = (int)values[ATTRIB_ID_TOTAL_SCORE_V1]; + LegacyTotalScore = (int)values[ATTRIB_ID_LEGACY_TOTAL_SCORE]; } } } From d10c63ed2de6e0dbacee501d17ff9b8e41cb0b0a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 8 Jun 2023 16:29:34 +0900 Subject: [PATCH 04/40] Fix difficulty calculation when mods are involved --- .../Difficulty/OsuDifficultyCalculator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1011066892..5292707ac1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -187,12 +187,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty int objectCount = countNormal + countSlider + countSpinner; DifficultyPeppyStars = (int)Math.Round( - (playableBeatmap.Difficulty.DrainRate - + playableBeatmap.Difficulty.OverallDifficulty - + playableBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / playableBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); - ScoreMultiplier = 1 * DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + ScoreMultiplier = DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); } } From 446807e7f67e89ee261393b1e367cead689b51bb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 12 Jun 2023 23:00:29 +0900 Subject: [PATCH 05/40] Add combo score / bonus score attributes --- .../Difficulty/OsuDifficultyCalculator.cs | 47 +++++++++++++++---- .../Difficulty/DifficultyAttributes.cs | 25 ++++++++-- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5292707ac1..2095738a81 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -93,11 +93,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; + OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + return new OsuDifficultyAttributes { StarRating = starRating, Mods = mods, - LegacyTotalScore = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods).TotalScore, AimDifficulty = aimRating, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, @@ -110,6 +111,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScore = sv1Processor.BonusScore }; } @@ -198,20 +202,38 @@ namespace osu.Game.Rulesets.Osu.Difficulty public class OsuScoreV1Processor : ScoreV1Processor { - public int TotalScore { get; private set; } + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + private int combo; public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) : base(baseBeatmap, playableBeatmap, mods) { foreach (var obj in playableBeatmap.HitObjects) - increaseScore(obj); + simulateHit(obj); } - private void increaseScore(HitObject hitObject) + private void simulateHit(HitObject hitObject) { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + int scoreIncrease = 0; switch (hitObject) @@ -229,11 +251,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty case SpinnerBonusTick: scoreIncrease = 1100; increaseCombo = false; + isBonus = true; break; case SpinnerTick: scoreIncrease = 100; increaseCombo = false; + isBonus = true; break; case HitCircle: @@ -243,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty case Slider: foreach (var nested in hitObject.NestedHitObjects) - increaseScore(nested); + simulateHit(nested); scoreIncrease = 300; increaseCombo = false; @@ -267,9 +291,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty for (int i = 0; i <= totalHalfSpinsPossible; i++) { if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) - increaseScore(new SpinnerBonusTick()); + simulateHit(new SpinnerBonusTick()); else if (i > 1 && i % 2 == 0) - increaseScore(new SpinnerTick()); + simulateHit(new SpinnerTick()); } scoreIncrease = 300; @@ -280,13 +304,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (addScoreComboMultiplier) { // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - scoreIncrease += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); + ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); } + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + if (increaseCombo) combo++; - - TotalScore += scoreIncrease; } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 8e30050a7f..5a51fb24a6 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Difficulty { @@ -27,6 +28,8 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; protected const int ATTRIB_ID_LEGACY_TOTAL_SCORE = 23; + protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25; + protected const int ATTRIB_ID_LEGACY_BONUS_SCORE = 27; /// /// The mods which were applied to the beatmap. @@ -36,21 +39,33 @@ namespace osu.Game.Rulesets.Difficulty /// /// The combined star rating of all skills. /// - [JsonProperty("star_rating", Order = -4)] + [JsonProperty("star_rating", Order = -7)] public double StarRating { get; set; } /// /// The maximum achievable combo. /// - [JsonProperty("max_combo", Order = -3)] + [JsonProperty("max_combo", Order = -6)] public int MaxCombo { get; set; } /// /// The maximum achievable legacy total score. /// - [JsonProperty("legacy_total_score", Order = -2)] + [JsonProperty("legacy_total_score", Order = -5)] public int LegacyTotalScore { get; set; } + /// + /// The combo-multiplied portion of . + /// + [JsonProperty("legacy_combo_score", Order = -4)] + public int LegacyComboScore { get; set; } + + /// + /// The "bonus" portion of consisting of all judgements that would be or . + /// + [JsonProperty("legacy_bonus_score", Order = -3)] + public int LegacyBonusScore { get; set; } + /// /// Creates new . /// @@ -79,6 +94,8 @@ namespace osu.Game.Rulesets.Difficulty { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_LEGACY_TOTAL_SCORE, LegacyTotalScore); + yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore); + yield return (ATTRIB_ID_LEGACY_BONUS_SCORE, LegacyBonusScore); } /// @@ -90,6 +107,8 @@ namespace osu.Game.Rulesets.Difficulty { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; LegacyTotalScore = (int)values[ATTRIB_ID_LEGACY_TOTAL_SCORE]; + LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE]; + LegacyBonusScore = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE]; } } } From b9f485b551f672c25721cbd28d9124f8f6fe1b6d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 12 Jun 2023 23:05:09 +0900 Subject: [PATCH 06/40] Merge classes + split out --- .../Difficulty/OsuDifficultyCalculator.cs | 164 ----------------- .../Difficulty/OsuScoreV1Processor.cs | 167 ++++++++++++++++++ 2 files changed, 167 insertions(+), 164 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 2095738a81..21ee03d1a5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -11,8 +11,6 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; @@ -154,166 +152,4 @@ namespace osu.Game.Rulesets.Osu.Difficulty new MultiMod(new OsuModFlashlight(), new OsuModHidden()) }; } - - public abstract class ScoreV1Processor - { - protected readonly int DifficultyPeppyStars; - protected readonly double ScoreMultiplier; - - protected readonly IBeatmap PlayableBeatmap; - - protected ScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) - { - PlayableBeatmap = playableBeatmap; - - int countNormal = 0; - int countSlider = 0; - int countSpinner = 0; - - foreach (HitObject obj in baseBeatmap.HitObjects) - { - switch (obj) - { - case IHasPath: - countSlider++; - break; - - case IHasDuration: - countSpinner++; - break; - - default: - countNormal++; - break; - } - } - - int objectCount = countNormal + countSlider + countSpinner; - - DifficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); - - ScoreMultiplier = DifficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); - } - } - - public class OsuScoreV1Processor : ScoreV1Processor - { - public int TotalScore => BaseScore + ComboScore + BonusScore; - - /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. - /// - public int ComboScore { get; private set; } - - /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. - /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } - - private int combo; - - public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) - : base(baseBeatmap, playableBeatmap, mods) - { - foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); - } - - private void simulateHit(HitObject hitObject) - { - bool increaseCombo = true; - bool addScoreComboMultiplier = false; - bool isBonus = false; - - int scoreIncrease = 0; - - switch (hitObject) - { - case SliderHeadCircle: - case SliderTailCircle: - case SliderRepeat: - scoreIncrease = 30; - break; - - case SliderTick: - scoreIncrease = 10; - break; - - case SpinnerBonusTick: - scoreIncrease = 1100; - increaseCombo = false; - isBonus = true; - break; - - case SpinnerTick: - scoreIncrease = 100; - increaseCombo = false; - isBonus = true; - break; - - case HitCircle: - scoreIncrease = 300; - addScoreComboMultiplier = true; - break; - - case Slider: - foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); - - scoreIncrease = 300; - increaseCombo = false; - addScoreComboMultiplier = true; - break; - - case Spinner spinner: - // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. - // We'll redo the calculations to match osu-stable here... - const double maximum_rotations_per_second = 477.0 / 60; - double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(PlayableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); - double secondsDuration = spinner.Duration / 1000; - - // The total amount of half spins possible for the entire spinner. - int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); - // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). - int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); - // To be able to receive bonus points, the spinner must be rotated another 1.5 times. - int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; - - for (int i = 0; i <= totalHalfSpinsPossible; i++) - { - if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) - simulateHit(new SpinnerBonusTick()); - else if (i > 1 && i % 2 == 0) - simulateHit(new SpinnerTick()); - } - - scoreIncrease = 300; - addScoreComboMultiplier = true; - break; - } - - if (addScoreComboMultiplier) - { - // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * ScoreMultiplier)); - } - - if (isBonus) - BonusScore += scoreIncrease; - else - BaseScore += scoreIncrease; - - if (increaseCombo) - combo++; - } - } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs new file mode 100644 index 0000000000..c82928b745 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -0,0 +1,167 @@ +// 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 System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Difficulty +{ + internal class OsuScoreV1Processor + { + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + + private int combo; + + private readonly double scoreMultiplier; + private readonly IBeatmap playableBeatmap; + + public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + this.playableBeatmap = playableBeatmap; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + int difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj); + } + + private void simulateHit(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + bool isBonus = false; + + int scoreIncrease = 0; + + switch (hitObject) + { + case SliderHeadCircle: + case SliderTailCircle: + case SliderRepeat: + scoreIncrease = 30; + break; + + case SliderTick: + scoreIncrease = 10; + break; + + case SpinnerBonusTick: + scoreIncrease = 1100; + increaseCombo = false; + isBonus = true; + break; + + case SpinnerTick: + scoreIncrease = 100; + increaseCombo = false; + isBonus = true; + break; + + case HitCircle: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case Slider: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + + scoreIncrease = 300; + increaseCombo = false; + addScoreComboMultiplier = true; + break; + + case Spinner spinner: + // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. + // We'll redo the calculations to match osu-stable here... + const double maximum_rotations_per_second = 477.0 / 60; + double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + double secondsDuration = spinner.Duration / 1000; + + // The total amount of half spins possible for the entire spinner. + int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); + // To be able to receive bonus points, the spinner must be rotated another 1.5 times. + int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; + + for (int i = 0; i <= totalHalfSpinsPossible; i++) + { + if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) + simulateHit(new SpinnerBonusTick()); + else if (i > 1 && i % 2 == 0) + simulateHit(new SpinnerTick()); + } + + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + } + + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + + if (increaseCombo) + combo++; + } + } +} From aa644832dccf4a979b67f53a605cb644f02fbaa8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Jun 2023 02:33:22 +0900 Subject: [PATCH 07/40] Add ScoreV1 calculation for TaikoRuleset --- .../TestSceneFlyingHits.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 8 + .../Difficulty/TaikoScoreV1Processor.cs | 196 ++++++++++++++++++ osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Objects/DrumRollTick.cs | 7 + 5 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs index e0ff617b59..88af50d36b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addFlyingHit(HitType hitType) { - var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + var tick = new DrumRollTick(null) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; DrawableDrumRollTick h; DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 24b5f5939a..28b07c0d59 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -27,9 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public override int Version => 20220902; + private readonly IWorkingBeatmap workingBeatmap; + public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; } protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) @@ -86,6 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + return new TaikoDifficultyAttributes { StarRating = starRating, @@ -96,6 +101,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScore = sv1Processor.BonusScore }; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs new file mode 100644 index 0000000000..ee52424b26 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -0,0 +1,196 @@ +// 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 System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty +{ + internal class TaikoScoreV1Processor + { + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + + private int combo; + + private readonly double modMultiplier; + private readonly int difficultyPeppyStars; + private readonly IBeatmap playableBeatmap; + private readonly IReadOnlyList mods; + + public TaikoScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + this.playableBeatmap = playableBeatmap; + this.mods = mods; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj); + } + + private void simulateHit(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + bool isBonus = false; + + int scoreIncrease = 0; + + switch (hitObject) + { + case SwellTick: + scoreIncrease = 300; + increaseCombo = false; + break; + + case DrumRollTick: + scoreIncrease = 300; + increaseCombo = false; + isBonus = true; + break; + + case Swell swell: + // The taiko swell generally does not match the osu-stable implementation in any way. + // We'll redo the calculations to match osu-stable here... + double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + double secondsDuration = swell.Duration / 1000; + + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); + + halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f); + + if (mods.Any(m => m is ModDoubleTime)) + halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f)); + if (mods.Any(m => m is ModHalfTime)) + halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f)); + + for (int i = 0; i <= halfSpinsRequiredForCompletion; i++) + simulateHit(new SwellTick()); + + scoreIncrease = 300; + addScoreComboMultiplier = true; + increaseCombo = false; + isBonus = true; + break; + + case Hit: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case DrumRoll: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + return; + } + + if (hitObject is DrumRollTick tick) + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(tick.Parent.StartTime).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + + if (tick.IsStrong) + scoreIncrease += scoreIncrease / 5; + } + + // The score increase directly contributed to by the combo-multiplied portion. + int comboScoreIncrease = 0; + + if (addScoreComboMultiplier) + { + int oldScoreIncrease = scoreIncrease; + + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10); + + if (hitObject is Swell) + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.GetEndTime()).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + } + else + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + } + + comboScoreIncrease = scoreIncrease - oldScoreIncrease; + } + + if (hitObject is Swell || (hitObject is TaikoStrongableHitObject strongable && strongable.IsStrong)) + { + scoreIncrease *= 2; + comboScoreIncrease *= 2; + } + + scoreIncrease -= comboScoreIncrease; + + if (addScoreComboMultiplier) + ComboScore += comboScoreIncrease; + + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + + if (increaseCombo) + combo++; + + if (hitObject is Swell) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 2f4a98bd8f..76d1a58506 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects { cancellationToken.ThrowIfCancellationRequested(); - AddNested(new DrumRollTick + AddNested(new DrumRollTick(this) { FirstTick = first, TickSpacing = tickSpacing, diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 206e8ecb5a..a8f309f7a6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Objects { public class DrumRollTick : TaikoStrongableHitObject { + public readonly DrumRoll Parent; + /// /// Whether this is the first (initial) tick of the slider. /// @@ -27,6 +29,11 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double HitWindow => TickSpacing / 2; + public DrumRollTick(DrumRoll parent) + { + Parent = parent; + } + public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; From 3ec97121e1a62bbfdec75de21d0afa6d1b94098b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Jun 2023 19:41:39 +0900 Subject: [PATCH 08/40] Add ScoreV1 calculation for CatchRuleset --- .../Difficulty/CatchDifficultyCalculator.cs | 8 ++ .../Difficulty/CatchScoreV1Processor.cs | 133 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 42cfde268e..fb7c4f05f4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty public override int Version => 20220701; + private readonly IWorkingBeatmap workingBeatmap; + public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -38,12 +41,17 @@ namespace osu.Game.Rulesets.Catch.Difficulty // this is the same as osu!, so there's potential to share the implementation... maybe double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + return new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScore = sv1Processor.BonusScore }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs new file mode 100644 index 0000000000..b5c3838fdc --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -0,0 +1,133 @@ +// 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 System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Catch.Difficulty +{ + internal class CatchScoreV1Processor + { + public int TotalScore => BaseScore + ComboScore + BonusScore; + + /// + /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// + public int ComboScore { get; private set; } + + /// + /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// + public int BaseScore { get; private set; } + + /// + /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. + /// + public int BonusScore { get; private set; } + + private int combo; + + private readonly double scoreMultiplier; + + public CatchScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + { + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + int difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj); + } + + private void simulateHit(HitObject hitObject) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + bool isBonus = false; + + int scoreIncrease = 0; + + switch (hitObject) + { + case TinyDroplet: + scoreIncrease = 10; + increaseCombo = false; + break; + + case Droplet: + scoreIncrease = 100; + break; + + case Fruit: + scoreIncrease = 300; + addScoreComboMultiplier = true; + increaseCombo = true; + break; + + case Banana: + scoreIncrease = 1100; + increaseCombo = false; + isBonus = true; + break; + + case JuiceStream: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + return; + + case BananaShower: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested); + return; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + } + + if (isBonus) + BonusScore += scoreIncrease; + else + BaseScore += scoreIncrease; + + if (increaseCombo) + combo++; + } + } +} From 13d1f9c902c68c708cde89cd162de8f0c9f3a1ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Jun 2023 23:22:27 +0900 Subject: [PATCH 09/40] Add ScoreV1 calculation for ManiaRuleset --- .../Difficulty/ManiaDifficultyCalculator.cs | 10 +++++++- .../Difficulty/ManiaScoreV1Processor.cs | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 63e61f17e3..cb41b93deb 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -33,9 +33,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; + private readonly IWorkingBeatmap workingBeatmap; + public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -48,6 +52,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty HitWindows hitWindows = new ManiaHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); + return new ManiaDifficultyAttributes { StarRating = skills[0].DifficultyValue() * star_scaling_factor, @@ -55,7 +61,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty // In osu-stable mania, rate-adjustment mods don't affect the hit window. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), - MaxCombo = beatmap.HitObjects.Sum(maxComboForObject) + MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), + LegacyTotalScore = sv1Processor.TotalScore, + LegacyComboScore = sv1Processor.TotalScore }; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs new file mode 100644 index 0000000000..5712205e8f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs @@ -0,0 +1,24 @@ +// 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.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + internal class ManiaScoreV1Processor + { + public int TotalScore { get; private set; } + + public ManiaScoreV1Processor(IReadOnlyList mods) + { + double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) + .Select(m => m.ScoreMultiplier) + .Aggregate((c, n) => c * n); + + TotalScore = (int)(1000000 * multiplier); + } + } +} From 975e9baf432dcb2af21869ad5f5e7fa47bfce34d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 15 Jun 2023 19:55:51 +0900 Subject: [PATCH 10/40] Fix exception with no matching mods --- osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs index 5712205e8f..f28a86b6b4 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) .Select(m => m.ScoreMultiplier) - .Aggregate((c, n) => c * n); + .Aggregate(1.0, (c, n) => c * n); TotalScore = (int)(1000000 * multiplier); } From bfa449e47ae959febca4c73c6edb035aabc99bc9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 19 Jun 2023 21:38:13 +0900 Subject: [PATCH 11/40] Adjust attribute data --- .../Difficulty/CatchDifficultyCalculator.cs | 4 +-- .../Difficulty/CatchScoreV1Processor.cs | 31 +++++++++++------- .../Difficulty/ManiaDifficultyCalculator.cs | 5 --- .../Difficulty/OsuDifficultyCalculator.cs | 4 +-- .../Difficulty/OsuScoreV1Processor.cs | 32 ++++++++++++------- .../Difficulty/TaikoDifficultyCalculator.cs | 4 +-- .../Difficulty/TaikoScoreV1Processor.cs | 32 ++++++++++++------- .../Difficulty/DifficultyAttributes.cs | 27 ++++++++-------- 8 files changed, 82 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index fb7c4f05f4..36af9fb980 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -49,9 +49,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), - LegacyTotalScore = sv1Processor.TotalScore, + LegacyAccuracyScore = sv1Processor.AccuracyScore, LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScore = sv1Processor.BonusScore + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index b5c3838fdc..3f0ac7a760 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -6,31 +6,34 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { internal class CatchScoreV1Processor { - public int TotalScore => BaseScore + ComboScore + BonusScore; + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore { get; private set; } /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// public int ComboScore { get; private set; } /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } + public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private int legacyBonusScore; + private int modernBonusScore; private int combo; private readonly double scoreMultiplier; @@ -77,7 +80,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + HitResult bonusResult = HitResult.None; int scoreIncrease = 0; @@ -102,6 +107,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty scoreIncrease = 1100; increaseCombo = false; isBonus = true; + bonusResult = HitResult.LargeBonus; break; case JuiceStream: @@ -122,9 +128,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty } if (isBonus) - BonusScore += scoreIncrease; + { + legacyBonusScore += scoreIncrease; + modernBonusScore += Judgement.ToNumericResult(bonusResult); + } else - BaseScore += scoreIncrease; + AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index cb41b93deb..d1058a9f8c 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -33,13 +33,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; - isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -62,7 +58,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), - LegacyTotalScore = sv1Processor.TotalScore, LegacyComboScore = sv1Processor.TotalScore }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 21ee03d1a5..5d6ed4792d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, - LegacyTotalScore = sv1Processor.TotalScore, + LegacyAccuracyScore = sv1Processor.AccuracyScore, LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScore = sv1Processor.BonusScore + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index c82928b745..28d029b73a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -5,32 +5,35 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { internal class OsuScoreV1Processor { - public int TotalScore => BaseScore + ComboScore + BonusScore; + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore { get; private set; } /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// public int ComboScore { get; private set; } /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } + public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private int legacyBonusScore; + private int modernBonusScore; private int combo; private readonly double scoreMultiplier; @@ -80,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + HitResult bonusResult = HitResult.None; int scoreIncrease = 0; @@ -100,12 +105,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty scoreIncrease = 1100; increaseCombo = false; isBonus = true; + bonusResult = HitResult.LargeBonus; break; case SpinnerTick: scoreIncrease = 100; increaseCombo = false; isBonus = true; + bonusResult = HitResult.SmallBonus; break; case HitCircle: @@ -156,9 +163,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty } if (isBonus) - BonusScore += scoreIncrease; + { + legacyBonusScore += scoreIncrease; + modernBonusScore += Judgement.ToNumericResult(bonusResult); + } else - BaseScore += scoreIncrease; + AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 28b07c0d59..49222adc89 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,9 +101,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), - LegacyTotalScore = sv1Processor.TotalScore, + LegacyAccuracyScore = sv1Processor.AccuracyScore, LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScore = sv1Processor.BonusScore + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index ee52424b26..23ff9585e8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -5,32 +5,35 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { internal class TaikoScoreV1Processor { - public int TotalScore => BaseScore + ComboScore + BonusScore; + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore { get; private set; } /// - /// Amount of score that is combo-and-difficulty-multiplied, excluding mod multipliers. + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// public int ComboScore { get; private set; } /// - /// Amount of score that is NOT combo-and-difficulty-multiplied. + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - public int BaseScore { get; private set; } - - /// - /// Amount of score whose judgements would be treated as "bonus" in ScoreV2. - /// - public int BonusScore { get; private set; } + public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private int legacyBonusScore; + private int modernBonusScore; private int combo; private readonly double modMultiplier; @@ -83,7 +86,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { bool increaseCombo = true; bool addScoreComboMultiplier = false; + bool isBonus = false; + HitResult bonusResult = HitResult.None; int scoreIncrease = 0; @@ -98,6 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty scoreIncrease = 300; increaseCombo = false; isBonus = true; + bonusResult = HitResult.SmallBonus; break; case Swell swell: @@ -123,6 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty addScoreComboMultiplier = true; increaseCombo = false; isBonus = true; + bonusResult = HitResult.LargeBonus; break; case Hit: @@ -181,9 +188,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ComboScore += comboScoreIncrease; if (isBonus) - BonusScore += scoreIncrease; + { + legacyBonusScore += scoreIncrease; + modernBonusScore += Judgement.ToNumericResult(bonusResult); + } else - BaseScore += scoreIncrease; + AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 5a51fb24a6..48e67ff425 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -27,9 +27,9 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; - protected const int ATTRIB_ID_LEGACY_TOTAL_SCORE = 23; + protected const int ATTRIB_ID_LEGACY_ACCURACY_SCORE = 23; protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25; - protected const int ATTRIB_ID_LEGACY_BONUS_SCORE = 27; + protected const int ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO = 27; /// /// The mods which were applied to the beatmap. @@ -49,22 +49,23 @@ namespace osu.Game.Rulesets.Difficulty public int MaxCombo { get; set; } /// - /// The maximum achievable legacy total score. + /// The accuracy portion of the legacy (ScoreV1) total score. /// - [JsonProperty("legacy_total_score", Order = -5)] - public int LegacyTotalScore { get; set; } + [JsonProperty("legacy_accuracy_score", Order = -5)] + public int LegacyAccuracyScore { get; set; } /// - /// The combo-multiplied portion of . + /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// [JsonProperty("legacy_combo_score", Order = -4)] public int LegacyComboScore { get; set; } /// - /// The "bonus" portion of consisting of all judgements that would be or . + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . /// - [JsonProperty("legacy_bonus_score", Order = -3)] - public int LegacyBonusScore { get; set; } + [JsonProperty("legacy_bonus_score_ratio", Order = -3)] + public double LegacyBonusScoreRatio { get; set; } /// /// Creates new . @@ -93,9 +94,9 @@ namespace osu.Game.Rulesets.Difficulty public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); - yield return (ATTRIB_ID_LEGACY_TOTAL_SCORE, LegacyTotalScore); + yield return (ATTRIB_ID_LEGACY_ACCURACY_SCORE, LegacyAccuracyScore); yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore); - yield return (ATTRIB_ID_LEGACY_BONUS_SCORE, LegacyBonusScore); + yield return (ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO, LegacyBonusScoreRatio); } /// @@ -106,9 +107,9 @@ namespace osu.Game.Rulesets.Difficulty public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; - LegacyTotalScore = (int)values[ATTRIB_ID_LEGACY_TOTAL_SCORE]; + LegacyAccuracyScore = (int)values[ATTRIB_ID_LEGACY_ACCURACY_SCORE]; LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE]; - LegacyBonusScore = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE]; + LegacyBonusScoreRatio = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO]; } } } From 87447f41d0f4fb9a527aad577c1b26586fff1a74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 24 Jun 2023 00:58:45 +0900 Subject: [PATCH 12/40] Fix incorrect calculation of difficulty --- .../Difficulty/CatchScoreV1Processor.cs | 10 +++++++++- .../Difficulty/OsuScoreV1Processor.cs | 10 +++++++++- .../Difficulty/TaikoScoreV1Processor.cs | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index 3f0ac7a760..be48763845 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -64,11 +64,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty int objectCount = countNormal + countSlider + countSpinner; + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + int difficultyPeppyStars = (int)Math.Round( (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index 28d029b73a..e8231794e0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -67,11 +67,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty int objectCount = countNormal + countSlider + countSpinner; + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + int difficultyPeppyStars = (int)Math.Round( (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index 23ff9585e8..f01ca74f4a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -70,11 +70,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty int objectCount = countNormal + countSlider + countSpinner; + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + difficultyPeppyStars = (int)Math.Round( (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / baseBeatmap.Difficulty.DrainRate * 8, 0, 16)) / 38 * 5); + + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); From 06565871d684549769ed28b30742f5eed609ea8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 24 Jun 2023 01:03:18 +0900 Subject: [PATCH 13/40] Add flag to disable computing legacy scoring values --- .../Difficulty/CatchDifficultyCalculator.cs | 17 +++++++++++------ .../Difficulty/ManiaDifficultyCalculator.cs | 13 +++++++++---- .../Difficulty/OsuDifficultyCalculator.cs | 17 +++++++++++------ .../Difficulty/TaikoDifficultyCalculator.cs | 17 +++++++++++------ .../Rulesets/Difficulty/DifficultyCalculator.cs | 7 +++++++ 5 files changed, 49 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 36af9fb980..a44aaf6dfa 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -41,18 +41,23 @@ namespace osu.Game.Rulesets.Catch.Difficulty // this is the same as osu!, so there's potential to share the implementation... maybe double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); - - return new CatchDifficultyAttributes + CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; + + if (ComputeLegacyScoringValues) + { + CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + } + + return attributes; } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index d1058a9f8c..675f6099e2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,9 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty HitWindows hitWindows = new ManiaHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); - - return new ManiaDifficultyAttributes + ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, @@ -58,8 +56,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), - LegacyComboScore = sv1Processor.TotalScore }; + + if (ComputeLegacyScoringValues) + { + ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); + attributes.LegacyComboScore = sv1Processor.TotalScore; + } + + return attributes; } private static int maxComboForObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5d6ed4792d..5158ea8a16 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -91,9 +91,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; - OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); - - return new OsuDifficultyAttributes + OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { StarRating = starRating, Mods = mods, @@ -109,10 +107,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; + + if (ComputeLegacyScoringValues) + { + OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + } + + return attributes; } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 49222adc89..d2f19e1e67 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -89,9 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); - - return new TaikoDifficultyAttributes + TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, Mods = mods, @@ -101,10 +99,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio }; + + if (ComputeLegacyScoringValues) + { + TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + } + + return attributes; } /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 00c90bd317..d005bbfc7a 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { + /// + /// Whether legacy scoring values (ScoreV1) should be computed to populate the difficulty attributes + /// , , + /// and . + /// + public bool ComputeLegacyScoringValues; + /// /// The beatmap for which difficulty will be calculated. /// From 8e79510793775ebab730d68d5fd962298aa77aa3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 26 Jun 2023 17:52:47 +0900 Subject: [PATCH 14/40] Add migration for total score conversion --- osu.Game/Database/RealmAccess.cs | 13 ++++++++++++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- osu.Game/Scoring/ScoreInfo.cs | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index da4caa42ba..e3423d25c5 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -78,8 +78,9 @@ namespace osu.Game.Database /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. + /// 31 2023-06-26 Add Version to ScoreInfo, set to 30000002. /// - private const int schema_version = 30; + private const int schema_version = 31; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -966,6 +967,16 @@ namespace osu.Game.Database break; } + + case 31: + { + var scores = migration.NewRealm.All(); + + foreach (var score in scores) + score.Version = 30000002; // Last version before legacy total score conversion. + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index f71da6c7e0..6c8b99b842 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -29,9 +29,10 @@ namespace osu.Game.Scoring.Legacy /// /// 30000001: Appends to the end of scores. /// 30000002: Score stored to replay calculated using the Score V2 algorithm. + /// 30000003: First version after legacy total score migration. /// /// - public const int LATEST_VERSION = 30000002; + public const int LATEST_VERSION = 30000003; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index d56338c6a4..fd67884956 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Users; using osu.Game.Utils; using Realms; @@ -63,6 +64,8 @@ namespace osu.Game.Scoring public double? PP { get; set; } + public int Version { get; set; } = LegacyScoreEncoder.LATEST_VERSION; + [Indexed] public long OnlineID { get; set; } = -1; From a9c65d200ae63be37d1a3663f9a9240d7d886397 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 26 Jun 2023 22:19:01 +0900 Subject: [PATCH 15/40] Initial conversion of scores --- .../Difficulty/CatchDifficultyCalculator.cs | 3 +- .../Difficulty/CatchScoreV1Processor.cs | 8 +- .../Difficulty/ManiaDifficultyCalculator.cs | 11 ++- .../Difficulty/ManiaScoreV1Processor.cs | 12 ++- .../Difficulty/OsuDifficultyCalculator.cs | 3 +- .../Difficulty/OsuScoreV1Processor.cs | 12 +-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 + .../Difficulty/TaikoDifficultyCalculator.cs | 3 +- .../Difficulty/TaikoScoreV1Processor.cs | 18 ++-- osu.Game/BackgroundBeatmapProcessor.cs | 88 +++++++++++++++++++ osu.Game/Rulesets/Ruleset.cs | 2 + .../Rulesets/Scoring/ILegacyScoreProcessor.cs | 30 +++++++ osu.Game/Scoring/ScoreImporter.cs | 58 ++++++++++++ osu.Game/Scoring/ScoreManager.cs | 2 + 14 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index a44aaf6dfa..5e562237c8 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (ComputeLegacyScoringValues) { - CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index be48763845..bda6be66a4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { - internal class CatchScoreV1Processor + internal class CatchScoreV1Processor : ILegacyScoreProcessor { /// /// The accuracy portion of the legacy (ScoreV1) total score. @@ -36,10 +36,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty private int modernBonusScore; private int combo; - private readonly double scoreMultiplier; + private double scoreMultiplier; - public CatchScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + int countNormal = 0; int countSlider = 0; int countSpinner = 0; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 4c419151c1..5403c1f860 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -31,9 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; + private IWorkingBeatmap workingBeatmap; + public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { + workingBeatmap = beatmap; + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -58,8 +62,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (ComputeLegacyScoringValues) { - ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(mods); - attributes.LegacyComboScore = sv1Processor.TotalScore; + ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; + attributes.LegacyComboScore = sv1Processor.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs index f28a86b6b4..9134ca4e2a 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs @@ -3,22 +3,26 @@ using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty { - internal class ManiaScoreV1Processor + internal class ManiaScoreV1Processor : ILegacyScoreProcessor { - public int TotalScore { get; private set; } + public int AccuracyScore => 0; + public int ComboScore { get; private set; } + public double BonusScoreRatio => 0; - public ManiaScoreV1Processor(IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) .Select(m => m.ScoreMultiplier) .Aggregate(1.0, (c, n) => c * n); - TotalScore = (int)(1000000 * multiplier); + ComboScore = (int)(1000000 * multiplier); } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5158ea8a16..7ecbb48ae6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -111,7 +111,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (ComputeLegacyScoringValues) { - OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index e8231794e0..aa52edae87 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { - internal class OsuScoreV1Processor + internal class OsuScoreV1Processor : ILegacyScoreProcessor { /// /// The accuracy portion of the legacy (ScoreV1) total score. @@ -36,18 +36,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int modernBonusScore; private int combo; - private readonly double scoreMultiplier; - private readonly IBeatmap playableBeatmap; + private double scoreMultiplier; + private IBeatmap playableBeatmap = null!; - public OsuScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { this.playableBeatmap = playableBeatmap; + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + int countNormal = 0; int countSlider = 0; int countSpinner = 0; - foreach (HitObject obj in baseBeatmap.HitObjects) + foreach (HitObject obj in workingBeatmap.Beatmap.HitObjects) { switch (obj) { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 4cff16b46f..c82f10c017 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -322,5 +322,7 @@ namespace osu.Game.Rulesets.Osu } public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); + + public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuScoreV1Processor(); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 1f34ba084f..b7f82b7512 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,7 +101,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (ComputeLegacyScoringValues) { - TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(workingBeatmap.Beatmap, beatmap, mods); + TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(); + sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index f01ca74f4a..255a3dd963 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { - internal class TaikoScoreV1Processor + internal class TaikoScoreV1Processor : ILegacyScoreProcessor { /// /// The accuracy portion of the legacy (ScoreV1) total score. @@ -36,16 +36,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int modernBonusScore; private int combo; - private readonly double modMultiplier; - private readonly int difficultyPeppyStars; - private readonly IBeatmap playableBeatmap; - private readonly IReadOnlyList mods; + private double modMultiplier; + private int difficultyPeppyStars; + private IBeatmap playableBeatmap = null!; + private IReadOnlyList mods = null!; - public TaikoScoreV1Processor(IBeatmap baseBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) { this.playableBeatmap = playableBeatmap; this.mods = mods; + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + int countNormal = 0; int countSlider = 0; int countSpinner = 0; @@ -205,10 +207,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (increaseCombo) combo++; - - if (hitObject is Swell) - { - } } } } diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index b8c89d8822..c49edec87d 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -14,6 +14,8 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -40,6 +42,9 @@ namespace osu.Game [Resolved] private ILocalUserPlayInfo? localUserPlayInfo { get; set; } + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + protected virtual int TimeToSleepDuringGameplay => 30000; protected override void LoadComplete() @@ -52,6 +57,7 @@ namespace osu.Game checkForOutdatedStarRatings(); processBeatmapSetsWithMissingMetrics(); processScoresWithMissingStatistics(); + convertLegacyTotalScoreToStandardised(); }).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -193,5 +199,87 @@ namespace osu.Game } } } + + private void convertLegacyTotalScoreToStandardised() + { + HashSet scoreIds = new HashSet(); + + Logger.Log("Querying for scores that need total score conversion..."); + + realmAccess.Run(r => + { + foreach (var score in r.All().Where(s => s.IsLegacyScore)) + { + if (score.RulesetID is not (0 or 1 or 2 or 3)) + continue; + + if (score.Version >= 30000003) + continue; + + scoreIds.Add(score.ID); + } + }); + + Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); + + ProgressNotification? notification = null; + + if (scoreIds.Count > 0) + notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); + + int count = 0; + updateNotification(); + + foreach (var id in scoreIds) + { + while (localUserPlayInfo?.IsPlaying.Value == true) + { + Logger.Log("Background processing sleeping due to active gameplay..."); + Thread.Sleep(TimeToSleepDuringGameplay); + } + + try + { + var score = scoreManager.Query(s => s.ID == id); + long newTotalScore = scoreManager.ConvertFromLegacyTotalScore(score); + + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + ScoreInfo s = r.Find(id); + s.TotalScore = newTotalScore; + s.Version = 30000003; + }); + + Logger.Log($"Converted total score for score {id}"); + } + catch (Exception e) + { + Logger.Log($"Failed to convert total score for {id}: {e}"); + } + + ++count; + updateNotification(); + } + + void updateNotification() + { + if (notification == null) + return; + + if (count == scoreIds.Count) + { + notification.CompletionText = $"Total score updated for {scoreIds.Count} scores"; + notification.Progress = 1; + notification.State = ProgressNotificationState.Completed; + } + else + { + notification.Text = $"Total score updated for {count} of {scoreIds.Count} scores"; + notification.Progress = (float)count / scoreIds.Count; + } + } + } } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 490ec1475c..5501a3a7c5 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -380,5 +380,7 @@ namespace osu.Game.Rulesets /// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen. /// public virtual RulesetSetupSection? CreateEditorSetupSection() => null; + + public virtual ILegacyScoreProcessor? CreateLegacyScoreProcessor() => null; } } diff --git a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs new file mode 100644 index 0000000000..70234a9b17 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs @@ -0,0 +1,30 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Scoring +{ + public interface ILegacyScoreProcessor + { + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + int AccuracyScore { get; } + + /// + /// The combo-multiplied portion of the legacy (ScoreV1) total score. + /// + int ComboScore { get; } + + /// + /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. + /// This is made up of all judgements that would be or . + /// + double BonusScoreRatio { get; } + + void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods); + } +} diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 16658a598a..04a8bc6fc5 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -88,6 +88,8 @@ namespace osu.Game.Scoring // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score. if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); + else if (model.IsLegacyScore) + model.TotalScore = ConvertFromLegacyTotalScore(model); } /// @@ -151,6 +153,62 @@ namespace osu.Game.Scoring #pragma warning restore CS0618 } + public long ConvertFromLegacyTotalScore(ScoreInfo score) + { + var beatmap = beatmaps().GetWorkingBeatmap(score.BeatmapInfo); + var ruleset = score.Ruleset.CreateInstance(); + + var sv1Processor = ruleset.CreateLegacyScoreProcessor(); + if (sv1Processor == null) + return score.TotalScore; + + sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + + int maximumLegacyAccuracyScore = sv1Processor.AccuracyScore; + int maximumLegacyComboScore = sv1Processor.ComboScore; + double maximumLegacyBonusRatio = sv1Processor.BonusScoreRatio; + double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + + // The part of total score that doesn't include bonus. + int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; + + // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. + double comboProportion = Math.Min(1, (double)score.TotalScore / maximumLegacyBaseScore); + + // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. + double bonusProportion = Math.Max(0, (score.TotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + + switch (ruleset.RulesetInfo.OnlineID) + { + case 0: + return (long)Math.Round(( + 700000 * comboProportion + + 300000 * Math.Pow(score.Accuracy, 10) + + bonusProportion) * modMultiplier); + + case 1: + return (long)Math.Round(( + 250000 * comboProportion + + 750000 * Math.Pow(score.Accuracy, 3.6) + + bonusProportion) * modMultiplier); + + case 2: + return (long)Math.Round(( + 600000 * comboProportion + + 400000 * score.Accuracy + + bonusProportion) * modMultiplier); + + case 3: + return (long)Math.Round(( + 990000 * comboProportion + + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + + bonusProportion) * modMultiplier); + + default: + return score.TotalScore; + } + } + // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). private readonly Dictionary usernameLookupCache = new Dictionary(); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 55bcb9f79d..fd5e9c851c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -169,6 +169,8 @@ namespace osu.Game.Scoring /// The score to populate the statistics of. public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); + public long ConvertFromLegacyTotalScore(ScoreInfo score) => scoreImporter.ConvertFromLegacyTotalScore(score); + #region Implementation of IPresentImports public Action>> PresentImport From 0c5c09597c382e7f6da25bb5c61d4972cafbf0ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 27 Jun 2023 14:59:40 +0900 Subject: [PATCH 16/40] Store old total score as LegacyTotalScore --- osu.Game/Database/RealmAccess.cs | 5 ++++- osu.Game/Scoring/ScoreImporter.cs | 7 +++++-- osu.Game/Scoring/ScoreInfo.cs | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e3423d25c5..727ddf06d7 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -78,7 +78,7 @@ namespace osu.Game.Database /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. - /// 31 2023-06-26 Add Version to ScoreInfo, set to 30000002. + /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and move TotalScore into LegacyTotalScore for legacy scores. /// private const int schema_version = 31; @@ -973,7 +973,10 @@ namespace osu.Game.Database var scores = migration.NewRealm.All(); foreach (var score in scores) + { + score.LegacyTotalScore = score.TotalScore; score.Version = 30000002; // Last version before legacy total score conversion. + } break; } diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 04a8bc6fc5..e8f23fdc10 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -155,6 +155,9 @@ namespace osu.Game.Scoring public long ConvertFromLegacyTotalScore(ScoreInfo score) { + if (!score.IsLegacyScore) + return score.TotalScore; + var beatmap = beatmaps().GetWorkingBeatmap(score.BeatmapInfo); var ruleset = score.Ruleset.CreateInstance(); @@ -173,10 +176,10 @@ namespace osu.Game.Scoring int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. - double comboProportion = Math.Min(1, (double)score.TotalScore / maximumLegacyBaseScore); + double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. - double bonusProportion = Math.Max(0, (score.TotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); switch (ruleset.RulesetInfo.OnlineID) { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index fd67884956..5de1c69d8a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -54,6 +54,8 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } + public long LegacyTotalScore { get; set; } + public int MaxCombo { get; set; } public double Accuracy { get; set; } From 5f350aa66fc118b563b2e9ca98f80f40245ca7fc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 27 Jun 2023 16:47:42 +0900 Subject: [PATCH 17/40] Fix float division Firstly, this is intended to be a float division. Secondly, dividing integers by 0 results in an exception, but dividing non-zero floats by 0 results in +/- infinity which will be clamped to the upper range. In particular, this occurs when the beatmap has 1 hitobject (0 drain length). --- osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs | 2 +- osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs index bda6be66a4..b4cca610c3 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs index aa52edae87..2e40d03fc0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs index 255a3dd963..eaa82e695e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty (baseBeatmap.Difficulty.DrainRate + baseBeatmap.Difficulty.OverallDifficulty + baseBeatmap.Difficulty.CircleSize - + Math.Clamp(objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); From 6e2369e6516ee3db92b9226f8a633d9c96b97773 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 27 Jun 2023 17:18:32 +0900 Subject: [PATCH 18/40] Add xmldoc on LegacyTotalScore --- osu.Game/Scoring/ScoreInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 5de1c69d8a..a0a0799fb1 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -54,6 +54,9 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } + /// + /// Used to preserve the total score for legacy scores. + /// public long LegacyTotalScore { get; set; } public int MaxCombo { get; set; } From e291dff5ad631a15e3b4beef63e785b41a574540 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 14:50:16 +0900 Subject: [PATCH 19/40] Fix imported scores not getting LegacyTotalScore --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c6461840aa..bf592d5988 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -123,6 +123,9 @@ namespace osu.Game.Scoring.Legacy PopulateAccuracy(score.ScoreInfo); + if (score.ScoreInfo.IsLegacyScore) + score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; + // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo; From 09bc8e45de43b44a4b0e235a99c8f69fac7624bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 15:04:13 +0900 Subject: [PATCH 20/40] Refactoring --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- ...cessor.cs => CatchLegacyScoreProcessor.cs} | 12 +-- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- ...cessor.cs => ManiaLegacyScoreProcessor.cs} | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- ...rocessor.cs => OsuLegacyScoreProcessor.cs} | 12 +-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- ...cessor.cs => TaikoLegacyScoreProcessor.cs} | 12 +-- osu.Game/BackgroundBeatmapProcessor.cs | 8 +- osu.Game/Database/RealmAccess.cs | 13 ++- .../StandardisedScoreMigrationTools.cs | 87 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 2 +- .../Rulesets/Scoring/ILegacyScoreProcessor.cs | 7 ++ osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 61 +------------ osu.Game/Scoring/ScoreInfo.cs | 11 +++ osu.Game/Scoring/ScoreManager.cs | 2 - 18 files changed, 133 insertions(+), 108 deletions(-) rename osu.Game.Rulesets.Catch/Difficulty/{CatchScoreV1Processor.cs => CatchLegacyScoreProcessor.cs} (88%) rename osu.Game.Rulesets.Mania/Difficulty/{ManiaScoreV1Processor.cs => ManiaLegacyScoreProcessor.cs} (93%) rename osu.Game.Rulesets.Osu/Difficulty/{OsuScoreV1Processor.cs => OsuLegacyScoreProcessor.cs} (91%) rename osu.Game.Rulesets.Taiko/Difficulty/{TaikoScoreV1Processor.cs => TaikoLegacyScoreProcessor.cs} (92%) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 5e562237c8..446a76486b 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (ComputeLegacyScoringValues) { - CatchScoreV1Processor sv1Processor = new CatchScoreV1Processor(); + CatchLegacyScoreProcessor sv1Processor = new CatchLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs similarity index 88% rename from osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs rename to osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs index b4cca610c3..67a813300d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchScoreV1Processor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs @@ -14,22 +14,12 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { - internal class CatchScoreV1Processor : ILegacyScoreProcessor + internal class CatchLegacyScoreProcessor : ILegacyScoreProcessor { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// public int AccuracyScore { get; private set; } - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// public int ComboScore { get; private set; } - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; private int legacyBonusScore; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 5403c1f860..e94e9b667d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (ComputeLegacyScoringValues) { - ManiaScoreV1Processor sv1Processor = new ManiaScoreV1Processor(); + ManiaLegacyScoreProcessor sv1Processor = new ManiaLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs similarity index 93% rename from osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs rename to osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs index 9134ca4e2a..e30d06c7b0 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaScoreV1Processor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty { - internal class ManiaScoreV1Processor : ILegacyScoreProcessor + internal class ManiaLegacyScoreProcessor : ILegacyScoreProcessor { public int AccuracyScore => 0; public int ComboScore { get; private set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 7ecbb48ae6..e28dbd96ac 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (ComputeLegacyScoringValues) { - OsuScoreV1Processor sv1Processor = new OsuScoreV1Processor(); + OsuLegacyScoreProcessor sv1Processor = new OsuLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs similarity index 91% rename from osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs index 2e40d03fc0..a5e12e5564 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuScoreV1Processor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs @@ -14,22 +14,12 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { - internal class OsuScoreV1Processor : ILegacyScoreProcessor + internal class OsuLegacyScoreProcessor : ILegacyScoreProcessor { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// public int AccuracyScore { get; private set; } - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// public int ComboScore { get; private set; } - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; private int legacyBonusScore; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c82f10c017..9b094ea1b1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -323,6 +323,6 @@ namespace osu.Game.Rulesets.Osu public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); - public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuScoreV1Processor(); + public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b7f82b7512..28268d9a13 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (ComputeLegacyScoringValues) { - TaikoScoreV1Processor sv1Processor = new TaikoScoreV1Processor(); + TaikoLegacyScoreProcessor sv1Processor = new TaikoLegacyScoreProcessor(); sv1Processor.Simulate(workingBeatmap, beatmap, mods); attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; attributes.LegacyComboScore = sv1Processor.ComboScore; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs similarity index 92% rename from osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs rename to osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs index eaa82e695e..c9f508f5e9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoScoreV1Processor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs @@ -14,22 +14,12 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { - internal class TaikoScoreV1Processor : ILegacyScoreProcessor + internal class TaikoLegacyScoreProcessor : ILegacyScoreProcessor { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// public int AccuracyScore { get; private set; } - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// public int ComboScore { get; private set; } - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; private int legacyBonusScore; diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index c49edec87d..44aceac1ca 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; namespace osu.Game @@ -27,6 +28,9 @@ namespace osu.Game [Resolved] private RulesetStore rulesetStore { get; set; } = null!; + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + [Resolved] private ScoreManager scoreManager { get; set; } = null!; @@ -241,7 +245,7 @@ namespace osu.Game try { var score = scoreManager.Query(s => s.ID == id); - long newTotalScore = scoreManager.ConvertFromLegacyTotalScore(score); + long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager); // Can't use async overload because we're not on the update thread. // ReSharper disable once MethodHasAsyncOverload @@ -249,7 +253,7 @@ namespace osu.Game { ScoreInfo s = r.Find(id); s.TotalScore = newTotalScore; - s.Version = 30000003; + s.Version = LegacyScoreEncoder.LATEST_VERSION; }); Logger.Log($"Converted total score for score {id}"); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 727ddf06d7..93d70d7aea 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -78,7 +78,7 @@ namespace osu.Game.Database /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. - /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and move TotalScore into LegacyTotalScore for legacy scores. + /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// private const int schema_version = 31; @@ -974,8 +974,15 @@ namespace osu.Game.Database foreach (var score in scores) { - score.LegacyTotalScore = score.TotalScore; - score.Version = 30000002; // Last version before legacy total score conversion. + if (score.IsLegacyScore) + { + score.LegacyTotalScore = score.TotalScore; + + // Scores with this version will trigger the update process in BackgroundBeatmapProcessor. + score.Version = 30000002; + } + else + score.Version = LegacyScoreEncoder.LATEST_VERSION; } break; diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 582a656efa..98e8671ede 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -185,6 +186,92 @@ namespace osu.Game.Database return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); } + /// + /// Converts from to the new standardised scoring of . + /// + /// The score to convert the total score of. + /// A used for lookups. + /// The standardised total score. + public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps) + { + if (!score.IsLegacyScore) + return score.TotalScore; + + var beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); + var ruleset = score.Ruleset.CreateInstance(); + + var sv1Processor = ruleset.CreateLegacyScoreProcessor(); + if (sv1Processor == null) + return score.TotalScore; + + sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + + return ConvertFromLegacyTotalScore(score, new DifficultyAttributes + { + LegacyAccuracyScore = sv1Processor.AccuracyScore, + LegacyComboScore = sv1Processor.ComboScore, + LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio + }); + } + + /// + /// Converts from to the new standardised scoring of . + /// + /// The score to convert the total score of. + /// Difficulty attributes providing the legacy scoring values + /// (, , and ) + /// for the beatmap which the score was set on. + /// The standardised total score. + public static long ConvertFromLegacyTotalScore(ScoreInfo score, DifficultyAttributes attributes) + { + if (!score.IsLegacyScore) + return score.TotalScore; + + int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore; + int maximumLegacyComboScore = attributes.LegacyComboScore; + double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio; + double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + + // The part of total score that doesn't include bonus. + int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; + + // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. + double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); + + // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. + double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + + switch (score.Ruleset.OnlineID) + { + case 0: + return (long)Math.Round(( + 700000 * comboProportion + + 300000 * Math.Pow(score.Accuracy, 10) + + bonusProportion) * modMultiplier); + + case 1: + return (long)Math.Round(( + 250000 * comboProportion + + 750000 * Math.Pow(score.Accuracy, 3.6) + + bonusProportion) * modMultiplier); + + case 2: + return (long)Math.Round(( + 600000 * comboProportion + + 400000 * score.Accuracy + + bonusProportion) * modMultiplier); + + case 3: + return (long)Math.Round(( + 990000 * comboProportion + + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + + bonusProportion) * modMultiplier); + + default: + return score.TotalScore; + } + } + private class FakeHit : HitObject { private readonly Judgement judgement; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6737caa5f9..cdd3b368bd 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -99,7 +99,7 @@ namespace osu.Game /// private const double global_track_volume_adjust = 0.8; - public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; + public virtual bool UseDevelopmentServer => false; public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration(); diff --git a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs index 70234a9b17..c689d3610d 100644 --- a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs @@ -25,6 +25,13 @@ namespace osu.Game.Rulesets.Scoring /// double BonusScoreRatio { get; } + /// + /// Performs the simulation, computing the maximum , , + /// and achievable for the given beatmap. + /// + /// The working beatmap. + /// A playable version of the beatmap for the ruleset. + /// The applied mods. void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods); } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 6c8b99b842..a5ac151cf8 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -29,7 +29,7 @@ namespace osu.Game.Scoring.Legacy /// /// 30000001: Appends to the end of scores. /// 30000002: Score stored to replay calculated using the Score V2 algorithm. - /// 30000003: First version after legacy total score migration. + /// 30000003: First version after converting legacy total score to standardised. /// /// public const int LATEST_VERSION = 30000003; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index e8f23fdc10..eb57f9a560 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -89,7 +89,7 @@ namespace osu.Game.Scoring if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); else if (model.IsLegacyScore) - model.TotalScore = ConvertFromLegacyTotalScore(model); + model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps()); } /// @@ -153,65 +153,6 @@ namespace osu.Game.Scoring #pragma warning restore CS0618 } - public long ConvertFromLegacyTotalScore(ScoreInfo score) - { - if (!score.IsLegacyScore) - return score.TotalScore; - - var beatmap = beatmaps().GetWorkingBeatmap(score.BeatmapInfo); - var ruleset = score.Ruleset.CreateInstance(); - - var sv1Processor = ruleset.CreateLegacyScoreProcessor(); - if (sv1Processor == null) - return score.TotalScore; - - sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); - - int maximumLegacyAccuracyScore = sv1Processor.AccuracyScore; - int maximumLegacyComboScore = sv1Processor.ComboScore; - double maximumLegacyBonusRatio = sv1Processor.BonusScoreRatio; - double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); - - // The part of total score that doesn't include bonus. - int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; - - // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. - double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); - - // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. - double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); - - switch (ruleset.RulesetInfo.OnlineID) - { - case 0: - return (long)Math.Round(( - 700000 * comboProportion - + 300000 * Math.Pow(score.Accuracy, 10) - + bonusProportion) * modMultiplier); - - case 1: - return (long)Math.Round(( - 250000 * comboProportion - + 750000 * Math.Pow(score.Accuracy, 3.6) - + bonusProportion) * modMultiplier); - - case 2: - return (long)Math.Round(( - 600000 * comboProportion - + 400000 * score.Accuracy - + bonusProportion) * modMultiplier); - - case 3: - return (long)Math.Round(( - 990000 * comboProportion - + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) - + bonusProportion) * modMultiplier); - - default: - return score.TotalScore; - } - } - // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). private readonly Dictionary usernameLookupCache = new Dictionary(); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a0a0799fb1..99b91318fd 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -57,6 +57,9 @@ namespace osu.Game.Scoring /// /// Used to preserve the total score for legacy scores. /// + /// + /// Not populated if is false. + /// public long LegacyTotalScore { get; set; } public int MaxCombo { get; set; } @@ -69,6 +72,14 @@ namespace osu.Game.Scoring public double? PP { get; set; } + /// + /// The version of this score as stored in the database. + /// If this does not match , + /// then the score has not yet been updated to reflect the current scoring values. + /// + /// + /// This may not match the version stored in the replay files. + /// public int Version { get; set; } = LegacyScoreEncoder.LATEST_VERSION; [Indexed] diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index fd5e9c851c..55bcb9f79d 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -169,8 +169,6 @@ namespace osu.Game.Scoring /// The score to populate the statistics of. public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); - public long ConvertFromLegacyTotalScore(ScoreInfo score) => scoreImporter.ConvertFromLegacyTotalScore(score); - #region Implementation of IPresentImports public Action>> PresentImport From af25ffbe8122587e437aeac0e42084412296d09c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 16:14:44 +0900 Subject: [PATCH 21/40] Remove JSON output --- osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 48e67ff425..5a01faa417 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -39,32 +39,29 @@ namespace osu.Game.Rulesets.Difficulty /// /// The combined star rating of all skills. /// - [JsonProperty("star_rating", Order = -7)] + [JsonProperty("star_rating", Order = -3)] public double StarRating { get; set; } /// /// The maximum achievable combo. /// - [JsonProperty("max_combo", Order = -6)] + [JsonProperty("max_combo", Order = -2)] public int MaxCombo { get; set; } /// /// The accuracy portion of the legacy (ScoreV1) total score. /// - [JsonProperty("legacy_accuracy_score", Order = -5)] public int LegacyAccuracyScore { get; set; } /// /// The combo-multiplied portion of the legacy (ScoreV1) total score. /// - [JsonProperty("legacy_combo_score", Order = -4)] public int LegacyComboScore { get; set; } /// /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. /// This is made up of all judgements that would be or . /// - [JsonProperty("legacy_bonus_score_ratio", Order = -3)] public double LegacyBonusScoreRatio { get; set; } /// From 1ca4e39fc33f090046bc0aa2fffe191d0bc24807 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 28 Jun 2023 16:30:50 +0900 Subject: [PATCH 22/40] Allow legacy scores to be displayed in "classic" scoring mode --- osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index e298d51ccb..980b742585 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -14,13 +14,7 @@ namespace osu.Game.Scoring.Legacy => getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics); public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode) - { - // Temporary to not scale stable scores that are already in the XX-millions with the classic scoring mode. - if (scoreInfo.IsLegacyScore) - return scoreInfo.TotalScore; - - return getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); - } + => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) { From 829044de59deed0445ed4670838fbd76cebb2508 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:15:48 +0900 Subject: [PATCH 23/40] Revert unintented change --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index cdd3b368bd..6737caa5f9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -99,7 +99,7 @@ namespace osu.Game /// private const double global_track_volume_adjust = 0.8; - public virtual bool UseDevelopmentServer => false; + public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration(); From c8162814945f48dcc233a45a6738678a1e4c688c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:16:33 +0900 Subject: [PATCH 24/40] Make BackgroundBeatmapProcessor task long-running --- osu.Game/BackgroundBeatmapProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 44aceac1ca..0b49bb26b2 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -55,14 +55,14 @@ namespace osu.Game { base.LoadComplete(); - Task.Run(() => + Task.Factory.StartNew(() => { Logger.Log("Beginning background beatmap processing.."); checkForOutdatedStarRatings(); processBeatmapSetsWithMissingMetrics(); processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); - }).ContinueWith(t => + }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) { From ddd870e843a26263e82bc5696ee1259fa47c2415 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:19:10 +0900 Subject: [PATCH 25/40] Make LegacyTotalScore nullable --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 5 ++++- osu.Game/Scoring/ScoreInfo.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 98e8671ede..c736c7e20e 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -227,6 +228,8 @@ namespace osu.Game.Database if (!score.IsLegacyScore) return score.TotalScore; + Debug.Assert(score.LegacyTotalScore != null); + int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore; int maximumLegacyComboScore = attributes.LegacyComboScore; double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio; @@ -239,7 +242,7 @@ namespace osu.Game.Database double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. - double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); switch (score.Ruleset.OnlineID) { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 99b91318fd..bdba81c685 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -60,7 +60,7 @@ namespace osu.Game.Scoring /// /// Not populated if is false. /// - public long LegacyTotalScore { get; set; } + public long? LegacyTotalScore { get; set; } public int MaxCombo { get; set; } From 6822871dab4dd6fc45667946c3308194dfcee8ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:21:24 +0900 Subject: [PATCH 26/40] Move population of LegacyTotalScore to ScoreImporter --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 --- osu.Game/Scoring/ScoreImporter.cs | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index bf592d5988..c6461840aa 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -123,9 +123,6 @@ namespace osu.Game.Scoring.Legacy PopulateAccuracy(score.ScoreInfo); - if (score.ScoreInfo.IsLegacyScore) - score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; - // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index eb57f9a560..5ada2a410d 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -89,7 +89,10 @@ namespace osu.Game.Scoring if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); else if (model.IsLegacyScore) + { + model.LegacyTotalScore = model.TotalScore; model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps()); + } } /// From c6ad184d94ae15a26a16825fd7620498ec133935 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:24:37 +0900 Subject: [PATCH 27/40] Move Ruleset method to ILegacyRuleset interface --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 ++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 ++ osu.Game/Database/StandardisedScoreMigrationTools.cs | 6 +++++- osu.Game/Rulesets/ILegacyRuleset.cs | 4 ++++ osu.Game/Rulesets/Ruleset.cs | 2 -- 7 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 8a0b8250d5..9862b7d886 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -202,6 +202,8 @@ namespace osu.Game.Rulesets.Catch public int LegacyID => 2; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new CatchLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index e8fda3ec80..77cc3e06d2 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -302,6 +302,8 @@ namespace osu.Game.Rulesets.Mania public int LegacyID => 3; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new ManiaLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 9b094ea1b1..abbd4a43c8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -253,6 +253,8 @@ namespace osu.Game.Rulesets.Osu public int LegacyID => 0; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo); @@ -322,7 +324,5 @@ namespace osu.Game.Rulesets.Osu } public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); - - public override ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index d6824109b3..af02c94d38 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -197,6 +197,8 @@ namespace osu.Game.Rulesets.Taiko public int LegacyID => 1; + public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new TaikoLegacyScoreProcessor(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo); diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index c736c7e20e..89bb908b1f 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -201,7 +202,10 @@ namespace osu.Game.Database var beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); var ruleset = score.Ruleset.CreateInstance(); - var sv1Processor = ruleset.CreateLegacyScoreProcessor(); + if (ruleset is not ILegacyRuleset legacyRuleset) + return score.TotalScore; + + var sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); if (sv1Processor == null) return score.TotalScore; diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index f4b03baccd..ba12c1f559 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Scoring; + namespace osu.Game.Rulesets { public interface ILegacyRuleset @@ -11,5 +13,7 @@ namespace osu.Game.Rulesets /// Identifies the server-side ID of a legacy ruleset. /// int LegacyID { get; } + + ILegacyScoreProcessor CreateLegacyScoreProcessor(); } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 5501a3a7c5..490ec1475c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -380,7 +380,5 @@ namespace osu.Game.Rulesets /// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen. /// public virtual RulesetSetupSection? CreateEditorSetupSection() => null; - - public virtual ILegacyScoreProcessor? CreateLegacyScoreProcessor() => null; } } From 426f11b824e770a901741dcda2385c719198eae3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Jun 2023 17:28:06 +0900 Subject: [PATCH 28/40] Apply a few other code reviews --- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs | 2 +- osu.Game/BackgroundBeatmapProcessor.cs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index e94e9b667d..d7994e6a0c 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; - private IWorkingBeatmap workingBeatmap; + private readonly IWorkingBeatmap workingBeatmap; public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs index 88af50d36b..0e10f75378 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addFlyingHit(HitType hitType) { - var tick = new DrumRollTick(null) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + var tick = new DrumRollTick(new DrumRoll()) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; DrawableDrumRollTick h; DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 0b49bb26b2..11e6a4619b 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; @@ -214,7 +215,7 @@ namespace osu.Game { foreach (var score in r.All().Where(s => s.IsLegacyScore)) { - if (score.RulesetID is not (0 or 1 or 2 or 3)) + if (!score.Ruleset.IsLegacyRuleset()) continue; if (score.Version >= 30000003) From 67650831bd07fe8756c5595d9117ca04064b9439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:19:25 +0900 Subject: [PATCH 29/40] Remove unnecessary null check --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 89bb908b1f..046563fad7 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -199,15 +199,13 @@ namespace osu.Game.Database if (!score.IsLegacyScore) return score.TotalScore; - var beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); - var ruleset = score.Ruleset.CreateInstance(); + WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); + Ruleset ruleset = score.Ruleset.CreateInstance(); if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; - var sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); - if (sv1Processor == null) - return score.TotalScore; + ILegacyScoreProcessor sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); From 1a6381bcbb26fc649a99a88cb07368ae0b249e7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 15:35:09 +0900 Subject: [PATCH 30/40] Reduce code repetition for sleep logic --- osu.Game/BackgroundBeatmapProcessor.cs | 33 +++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 11e6a4619b..9a2d029724 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -132,11 +132,7 @@ namespace osu.Game foreach (var id in beatmapSetIds) { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } + sleepIfRequired(); realmAccess.Run(r => { @@ -177,11 +173,7 @@ namespace osu.Game foreach (var id in scoreIds) { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } + sleepIfRequired(); try { @@ -229,19 +221,17 @@ namespace osu.Game ProgressNotification? notification = null; - if (scoreIds.Count > 0) - notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); + if (scoreIds.Count == 0) + return; + + notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); int count = 0; updateNotification(); foreach (var id in scoreIds) { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } + sleepIfRequired(); try { @@ -286,5 +276,14 @@ namespace osu.Game } } } + + private void sleepIfRequired() + { + while (localUserPlayInfo?.IsPlaying.Value == true) + { + Logger.Log("Background processing sleeping due to active gameplay..."); + Thread.Sleep(TimeToSleepDuringGameplay); + } + } } } From 3b5f3b67a7672a07896ce50a83a43d1b089a2ff0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 15:42:04 +0900 Subject: [PATCH 31/40] Tidy up and improve messaging on completion notification --- osu.Game/BackgroundBeatmapProcessor.cs | 40 +++++++++++--------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9a2d029724..b3fb938f48 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -219,18 +219,20 @@ namespace osu.Game Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); - ProgressNotification? notification = null; - if (scoreIds.Count == 0) return; - notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); + ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active }; - int count = 0; - updateNotification(); + notificationOverlay?.Post(notification); + + int processedCount = 0; foreach (var id in scoreIds) { + notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})"; + notification.Progress = (float)processedCount / scoreIds.Count; + sleepIfRequired(); try @@ -248,32 +250,24 @@ namespace osu.Game }); Logger.Log($"Converted total score for score {id}"); + ++processedCount; } catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); } - - ++count; - updateNotification(); } - void updateNotification() + if (processedCount == scoreIds.Count) { - if (notification == null) - return; - - if (count == scoreIds.Count) - { - notification.CompletionText = $"Total score updated for {scoreIds.Count} scores"; - notification.Progress = 1; - notification.State = ProgressNotificationState.Completed; - } - else - { - notification.Text = $"Total score updated for {count} of {scoreIds.Count} scores"; - notification.Progress = (float)count / scoreIds.Count; - } + notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm"; + notification.Progress = 1; + notification.State = ProgressNotificationState.Completed; + } + else + { + notification.CompletionText = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm. Check logs for issues with remaining scores."; + notification.State = ProgressNotificationState.Cancelled; } } From 16290241114691382a91116ba15e926043496afb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 17:32:54 +0900 Subject: [PATCH 32/40] `ILegacyScoreProcessor` -> `ILegacyScoreSimulator` --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Difficulty/CatchDifficultyCalculator.cs | 10 +++++----- ...yScoreProcessor.cs => CatchLegacyScoreSimulator.cs} | 2 +- .../Difficulty/ManiaDifficultyCalculator.cs | 10 +++++----- ...yScoreProcessor.cs => ManiaLegacyScoreSimulator.cs} | 2 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 10 +++++----- ...acyScoreProcessor.cs => OsuLegacyScoreSimulator.cs} | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 10 +++++----- ...yScoreProcessor.cs => TaikoLegacyScoreSimulator.cs} | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Database/StandardisedScoreMigrationTools.cs | 10 +++++----- osu.Game/Rulesets/ILegacyRuleset.cs | 2 +- ...egacyScoreProcessor.cs => ILegacyScoreSimulator.cs} | 5 ++++- 15 files changed, 38 insertions(+), 35 deletions(-) rename osu.Game.Rulesets.Catch/Difficulty/{CatchLegacyScoreProcessor.cs => CatchLegacyScoreSimulator.cs} (98%) rename osu.Game.Rulesets.Mania/Difficulty/{ManiaLegacyScoreProcessor.cs => ManiaLegacyScoreSimulator.cs} (93%) rename osu.Game.Rulesets.Osu/Difficulty/{OsuLegacyScoreProcessor.cs => OsuLegacyScoreSimulator.cs} (99%) rename osu.Game.Rulesets.Taiko/Difficulty/{TaikoLegacyScoreProcessor.cs => TaikoLegacyScoreSimulator.cs} (99%) rename osu.Game/Rulesets/Scoring/{ILegacyScoreProcessor.cs => ILegacyScoreSimulator.cs} (90%) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9862b7d886..8f1a1b8ef5 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -202,7 +202,7 @@ namespace osu.Game.Rulesets.Catch public int LegacyID => 2; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new CatchLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 446a76486b..0b56405299 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -51,11 +51,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (ComputeLegacyScoringValues) { - CatchLegacyScoreProcessor sv1Processor = new CatchLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs similarity index 98% rename from osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs index 67a813300d..c79fd36d96 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Difficulty { - internal class CatchLegacyScoreProcessor : ILegacyScoreProcessor + internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore { get; private set; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index d7994e6a0c..de9f0d91ae 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -62,11 +62,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (ComputeLegacyScoringValues) { - ManiaLegacyScoreProcessor sv1Processor = new ManiaLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs similarity index 93% rename from osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs index e30d06c7b0..e544428979 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty { - internal class ManiaLegacyScoreProcessor : ILegacyScoreProcessor + internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore => 0; public int ComboScore { get; private set; } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 77cc3e06d2..2e96c89516 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Mania public int LegacyID => 3; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new ManiaLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index e28dbd96ac..b92092c674 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -111,11 +111,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (ComputeLegacyScoringValues) { - OsuLegacyScoreProcessor sv1Processor = new OsuLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs similarity index 99% rename from osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs index a5e12e5564..980d86e4ad 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { - internal class OsuLegacyScoreProcessor : ILegacyScoreProcessor + internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore { get; private set; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index abbd4a43c8..b44d999d4f 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu public int LegacyID => 0; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new OsuLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 28268d9a13..25adba5ab6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (ComputeLegacyScoringValues) { - TaikoLegacyScoreProcessor sv1Processor = new TaikoLegacyScoreProcessor(); - sv1Processor.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Processor.AccuracyScore; - attributes.LegacyComboScore = sv1Processor.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio; + TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator(); + sv1Simulator.Simulate(workingBeatmap, beatmap, mods); + attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; + attributes.LegacyComboScore = sv1Simulator.ComboScore; + attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; } return attributes; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs similarity index 99% rename from osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs rename to osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index c9f508f5e9..e77327d622 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { - internal class TaikoLegacyScoreProcessor : ILegacyScoreProcessor + internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator { public int AccuracyScore { get; private set; } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index af02c94d38..aa31b1924f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Taiko public int LegacyID => 1; - public ILegacyScoreProcessor CreateLegacyScoreProcessor() => new TaikoLegacyScoreProcessor(); + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 046563fad7..7ab90c337c 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -205,15 +205,15 @@ namespace osu.Game.Database if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; - ILegacyScoreProcessor sv1Processor = legacyRuleset.CreateLegacyScoreProcessor(); + ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); - sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + sv1Simulator.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); return ConvertFromLegacyTotalScore(score, new DifficultyAttributes { - LegacyAccuracyScore = sv1Processor.AccuracyScore, - LegacyComboScore = sv1Processor.ComboScore, - LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio + LegacyAccuracyScore = sv1Simulator.AccuracyScore, + LegacyComboScore = sv1Simulator.ComboScore, + LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio }); } diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index ba12c1f559..24aa672219 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets /// int LegacyID { get; } - ILegacyScoreProcessor CreateLegacyScoreProcessor(); + ILegacyScoreSimulator CreateLegacyScoreSimulator(); } } diff --git a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs similarity index 90% rename from osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs rename to osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs index c689d3610d..7240f0d73e 100644 --- a/osu.Game/Rulesets/Scoring/ILegacyScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs @@ -7,7 +7,10 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Scoring { - public interface ILegacyScoreProcessor + /// + /// Generates attributes which are required to calculate old-style Score V1 scores. + /// + public interface ILegacyScoreSimulator { /// /// The accuracy portion of the legacy (ScoreV1) total score. From a0c3fa9c138a15f668b06a5ca5eea3b1a7722090 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 17:53:53 +0900 Subject: [PATCH 33/40] Move preconditions to realm migration step to simplify marker version logic --- osu.Game/BackgroundBeatmapProcessor.cs | 17 +---------------- osu.Game/Database/RealmAccess.cs | 13 ++++++------- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index b3fb938f48..3af6f0771c 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; @@ -199,23 +198,9 @@ namespace osu.Game private void convertLegacyTotalScoreToStandardised() { - HashSet scoreIds = new HashSet(); - Logger.Log("Querying for scores that need total score conversion..."); - realmAccess.Run(r => - { - foreach (var score in r.All().Where(s => s.IsLegacyScore)) - { - if (!score.Ruleset.IsLegacyRuleset()) - continue; - - if (score.Version >= 30000003) - continue; - - scoreIds.Add(score.ID); - } - }); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 93d70d7aea..95297e9cd6 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -970,16 +970,15 @@ namespace osu.Game.Database case 31: { - var scores = migration.NewRealm.All(); - - foreach (var score in scores) + foreach (var score in migration.NewRealm.All()) { - if (score.IsLegacyScore) + if (score.IsLegacyScore && score.Ruleset.IsLegacyRuleset()) { - score.LegacyTotalScore = score.TotalScore; - - // Scores with this version will trigger the update process in BackgroundBeatmapProcessor. + // Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor. score.Version = 30000002; + + // Set a sane default while background processing runs. + score.LegacyTotalScore = score.TotalScore; } else score.Version = LegacyScoreEncoder.LATEST_VERSION; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index a5ac151cf8..ef033bf5bd 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -28,7 +28,7 @@ namespace osu.Game.Scoring.Legacy /// /// /// 30000001: Appends to the end of scores. - /// 30000002: Score stored to replay calculated using the Score V2 algorithm. + /// 30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion. /// 30000003: First version after converting legacy total score to standardised. /// /// From 4de15f975e1acf5bd91077f8464f35aba68c3805 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:08:26 +0900 Subject: [PATCH 34/40] Fix realm silly business --- osu.Game/BackgroundBeatmapProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 3af6f0771c..0b8323eb41 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -200,7 +200,7 @@ namespace osu.Game { Logger.Log("Querying for scores that need total score conversion..."); - HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); From 257a96ef604a494122ec564a20405bbe6ad63be8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:21:22 +0900 Subject: [PATCH 35/40] Fix background beatmap processor thread not correctly exiting --- osu.Game/BackgroundBeatmapProcessor.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 0b8323eb41..f5e3f721f7 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -189,6 +189,10 @@ namespace osu.Game Logger.Log($"Populated maximum statistics for score {id}"); } + catch (ObjectDisposedException) + { + throw; + } catch (Exception e) { Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); @@ -237,6 +241,10 @@ namespace osu.Game Logger.Log($"Converted total score for score {id}"); ++processedCount; } + catch (ObjectDisposedException) + { + throw; + } catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); From 56bfb92ba656ccb216bc10c64a1fb5e0144bc847 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:22:10 +0900 Subject: [PATCH 36/40] Allow user cancellation --- osu.Game/BackgroundBeatmapProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index f5e3f721f7..ca33a74c57 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -219,6 +219,9 @@ namespace osu.Game foreach (var id in scoreIds) { + if (notification.State == ProgressNotificationState.Cancelled) + break; + notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})"; notification.Progress = (float)processedCount / scoreIds.Count; From d3eb06578e96a75be4521fe92a461d043aaa3d45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:34:53 +0900 Subject: [PATCH 37/40] Improve messaging around failed scores --- osu.Game/BackgroundBeatmapProcessor.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index ca33a74c57..018b1352b2 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -216,6 +216,7 @@ namespace osu.Game notificationOverlay?.Post(notification); int processedCount = 0; + int failedCount = 0; foreach (var id in scoreIds) { @@ -251,6 +252,7 @@ namespace osu.Game catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); + ++failedCount; } } @@ -262,7 +264,12 @@ namespace osu.Game } else { - notification.CompletionText = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm. Check logs for issues with remaining scores."; + notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm."; + + // We may have arrived here due to user cancellation or completion with failures. + if (failedCount > 0) + notification.Text += $" Check logs for issues with {failedCount} failed upgrades."; + notification.State = ProgressNotificationState.Cancelled; } } From dd9998127eb8e1d9899bca50cbd6b6fbf8af0650 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:35:03 +0900 Subject: [PATCH 38/40] Count missing beatmaps as errored items --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 7ab90c337c..60530c31cb 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -205,9 +205,14 @@ namespace osu.Game.Database if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; + var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); + + if (playableBeatmap.HitObjects.Count == 0) + throw new InvalidOperationException("Beatmap contains no hit objects!"); + ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); - sv1Simulator.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods); + sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods); return ConvertFromLegacyTotalScore(score, new DifficultyAttributes { From aee89e5e4bacfbfe4263f941457f28837a595135 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 19:59:57 +0900 Subject: [PATCH 39/40] Rewrite comment regarding `LegacyTotalScore` --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 95297e9cd6..02abed2495 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -977,7 +977,7 @@ namespace osu.Game.Database // Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor. score.Version = 30000002; - // Set a sane default while background processing runs. + // Transfer known legacy scores to a permanent storage field for preservation. score.LegacyTotalScore = score.TotalScore; } else From f2aa80f4138452c4a859e463f780c1c87b106cc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 20:02:25 +0900 Subject: [PATCH 40/40] Rename and adjust xmldoc on `TotalScoreVersion` --- osu.Game/BackgroundBeatmapProcessor.cs | 4 ++-- osu.Game/Database/RealmAccess.cs | 4 ++-- osu.Game/Scoring/ScoreInfo.cs | 24 +++++++++++++----------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 018b1352b2..9fe3a41b03 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -204,7 +204,7 @@ namespace osu.Game { Logger.Log("Querying for scores that need total score conversion..."); - HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.Version == 30000002).AsEnumerable().Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All().Where(s => s.TotalScoreVersion == 30000002).AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); @@ -239,7 +239,7 @@ namespace osu.Game { ScoreInfo s = r.Find(id); s.TotalScore = newTotalScore; - s.Version = LegacyScoreEncoder.LATEST_VERSION; + s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); Logger.Log($"Converted total score for score {id}"); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 02abed2495..2bc932f307 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -975,13 +975,13 @@ namespace osu.Game.Database if (score.IsLegacyScore && score.Ruleset.IsLegacyRuleset()) { // Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor. - score.Version = 30000002; + score.TotalScoreVersion = 30000002; // Transfer known legacy scores to a permanent storage field for preservation. score.LegacyTotalScore = score.TotalScore; } else - score.Version = LegacyScoreEncoder.LATEST_VERSION; + score.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; } break; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 94376300fa..eddd1bb80a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -54,13 +54,25 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } + /// + /// The version of processing applied to calculate total score as stored in the database. + /// If this does not match , + /// the total score has not yet been updated to reflect the current scoring values. + /// + /// See 's conversion logic. + /// + /// + /// This may not match the version stored in the replay files. + /// + internal int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION; + /// /// Used to preserve the total score for legacy scores. /// /// /// Not populated if is false. /// - public long? LegacyTotalScore { get; set; } + internal long? LegacyTotalScore { get; set; } public int MaxCombo { get; set; } @@ -72,16 +84,6 @@ namespace osu.Game.Scoring public double? PP { get; set; } - /// - /// The version of this score as stored in the database. - /// If this does not match , - /// then the score has not yet been updated to reflect the current scoring values. - /// - /// - /// This may not match the version stored in the replay files. - /// - public int Version { get; set; } = LegacyScoreEncoder.LATEST_VERSION; - [Indexed] public long OnlineID { get; set; } = -1;