From 02111e38542edaa0657c9e1ea7ac48ed4b0d10f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 1 Jun 2023 13:22:37 +0900 Subject: [PATCH 01/60] 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/60] 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/60] "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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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 e87cf6d2567da36e72e7f983e216daed68fd78db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:06:32 +0900 Subject: [PATCH 29/60] Move all remaining osu!taiko sample playback logic out of `DrawableHitObject`s --- .../Objects/Drawables/DrawableHit.cs | 36 ------------------- .../Drawables/DrawableTaikoHitObject.cs | 4 +-- .../UI/DrumSampleTriggerSource.cs | 19 +++++++--- 3 files changed, 17 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 62c8457c58..5b79151225 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -4,14 +4,12 @@ #nullable disable using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -93,40 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit) : new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); - public override IEnumerable GetSamples() - { - // normal and claps are always handled by the drum (see DrumSampleMapping). - // in addition, whistles are excluded as they are an alternative rim marker. - - var samples = HitObject.Samples.Where(s => - s.Name != HitSampleInfo.HIT_NORMAL - && s.Name != HitSampleInfo.HIT_CLAP - && s.Name != HitSampleInfo.HIT_WHISTLE); - - if (HitObject.Type == HitType.Rim && HitObject.IsStrong) - { - // strong + rim always maps to whistle. - // TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken. - // when we add a taiko editor, this is probably not going to play nice. - - var corrected = samples.ToList(); - - for (int i = 0; i < corrected.Count; i++) - { - var s = corrected[i]; - - if (s.Name != HitSampleInfo.HIT_FINISH) - continue; - - corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE); - } - - return corrected; - } - - return samples; - } - protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 1b5d641612..3f4694d71d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool RemoveWhenNotAlive => false; } - // Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource). - public override IEnumerable GetSamples() => Enumerable.Empty(); + // osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource). + public sealed override IEnumerable GetSamples() => Enumerable.Empty(); } public abstract partial class DrawableTaikoHitObject : DrawableTaikoHitObject diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 92f2b74568..c732cc000f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; +using System.Collections.Generic; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; @@ -18,12 +18,23 @@ namespace osu.Game.Rulesets.Taiko.UI public void Play(HitType hitType) { - var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; - if (hitSample == null) + if (hitObject == null) return; - PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) }); + List samplesToPlay = new List + { + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) + }; + + // strong + rim always maps to whistle. + if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true) + { + samplesToPlay.Add(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)); + } + + PlaySamples(samplesToPlay.ToArray()); } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); From c98abf1723f6bcc32896adb6ac1e9864c1826299 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:38:17 +0900 Subject: [PATCH 30/60] More correctly handle `StrongNestedHitObject`s --- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index c732cc000f..adf02d88ce 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.UI }; // strong + rim always maps to whistle. - if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true) + if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { samplesToPlay.Add(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)); } From 32c0f13f79b1c6474e08d70721ef12deee088607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:38:24 +0900 Subject: [PATCH 31/60] Update tests to match new expectations --- .../TestSceneDrumSampleTriggerSource.cs | 117 +++++++++--------- .../TestSceneSampleOutput.cs | 15 +-- 2 files changed, 67 insertions(+), 65 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index bce855ae45..4133b96d42 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(120); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(480); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(700); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests StartTime = 100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), - new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong } }; hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -272,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests EndTime = 1100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), - new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong } }; drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests EndTime = 1100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM) } }; swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -356,25 +356,26 @@ namespace osu.Game.Rulesets.Taiko.Tests // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); } - private void checkSound(HitType hitType, string expectedName, string expectedBank) + private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank) { AddStep($"hit {hitType}", () => triggerSource.Play(hitType)); - AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType().Single().Name, () => Is.EqualTo(expectedName)); - AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().Single().Bank, () => Is.EqualTo(expectedBank)); + AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType().Select(s => s.Name)), + () => Is.EqualTo(expectedSamplesCsv)); + AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().First().Bank, () => Is.EqualTo(expectedBank)); } private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time)); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index 2429b71095..a548a14d88 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -3,15 +3,16 @@ using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Tests { /// - /// Taiko has some interesting rules for legacy mappings. + /// Taiko doesn't output any samples. They are all handled externally by . /// [HeadlessTest] public partial class TestSceneSampleOutput : TestSceneTaikoPlayer @@ -26,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests string.Empty, string.Empty, string.Empty, - HitSampleInfo.HIT_FINISH, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, + string.Empty, + string.Empty, + string.Empty, + string.Empty, }; var actualSampleNames = new List(); @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length); - AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames)); + AddAssert("samples are correct", () => actualSampleNames, () => Is.EqualTo(expectedSampleNames)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions"); From 571dbf5ab8d58e4d5712f18da4c400ca6f7c3f8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:42:39 +0900 Subject: [PATCH 32/60] Adjust logic to avoid creating `List<>` each playback --- .../UI/DrumSampleTriggerSource.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index adf02d88ce..a04c4b60f2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; @@ -23,18 +22,20 @@ namespace osu.Game.Rulesets.Taiko.UI if (hitObject == null) return; - List samplesToPlay = new List - { - hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) - }; + var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); - // strong + rim always maps to whistle. if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { - samplesToPlay.Add(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)); + PlaySamples(new ISampleInfo[] + { + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), + baseSample + }); + } + else + { + PlaySamples(new ISampleInfo[] { baseSample }); } - - PlaySamples(samplesToPlay.ToArray()); } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); From 67650831bd07fe8756c5595d9117ca04064b9439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:19:25 +0900 Subject: [PATCH 33/60] 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 34/60] 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 35/60] 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 36/60] `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 37/60] 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 38/60] 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 39/60] 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 40/60] 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 41/60] 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 42/60] 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 664294cef4728d4b35d51fa58cf9480913a2a10d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 18:39:19 +0900 Subject: [PATCH 43/60] Fix cancelled progress notifications requiring exit confirmation --- osu.Game/Overlays/INotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index 6a1b66bbd2..c5ff10c619 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -44,6 +44,6 @@ namespace osu.Game.Overlays /// /// All ongoing operations (ie. any not in a completed state). /// - public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.State != ProgressNotificationState.Completed); + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.State != ProgressNotificationState.Completed && p.State != ProgressNotificationState.Cancelled); } } From aee89e5e4bacfbfe4263f941457f28837a595135 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 19:59:57 +0900 Subject: [PATCH 44/60] 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 45/60] 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; From 6dc8c7b617038a87898c0a847788c4cd502c7c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:26:41 +0200 Subject: [PATCH 46/60] Add `HitObjectLifetimeEntry.NestedEntries` --- osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index b517f6b9e6..4e058e7c31 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; @@ -19,6 +20,11 @@ namespace osu.Game.Rulesets.Objects /// public readonly HitObject HitObject; + /// + /// The list of for the 's nested objects (if any). + /// + public readonly List NestedEntries = new List(); + /// /// The result that was judged with. /// This is set by the accompanying , and reused when required for rewinding. From 2b098bdf6120120e8f1985bf98d16ea26a578f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 23:23:23 +0200 Subject: [PATCH 47/60] Add test coverage for mixed pooled/non-pooled usages --- .../Gameplay/TestScenePoolingRuleset.cs | 85 ++++++++++++++++--- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index d16f51f36e..e3afb91040 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -170,7 +170,16 @@ namespace osu.Game.Tests.Visual.Gameplay ManualClock clock = null; var beatmap = new Beatmap(); - beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 }); + beatmap.HitObjects.Add(new TestHitObjectWithNested + { + Duration = 40, + NestedObjects = new HitObject[] + { + new PooledNestedHitObject { StartTime = 10 }, + new PooledNestedHitObject { StartTime = 20 }, + new PooledNestedHitObject { StartTime = 30 } + } + }); createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); @@ -209,6 +218,44 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("object judged", () => playfield.JudgedObjects.Count == 1); } + [Test] + public void TestPooledObjectWithNonPooledNesteds() + { + ManualClock clock = null; + TestHitObjectWithNested hitObjectWithNested; + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(hitObjectWithNested = new TestHitObjectWithNested + { + Duration = 40, + NestedObjects = new HitObject[] + { + new PooledNestedHitObject { StartTime = 10 }, + new NonPooledNestedHitObject { StartTime = 20 }, + new NonPooledNestedHitObject { StartTime = 30 } + } + }); + + createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); + + AddAssert("hitobject entry has all nesteds", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(3)); + + AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2); + AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2)); + + AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1); + AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + + AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested)); + AddStep("clear judged", () => playfield.JudgedObjects.Clear()); + AddStep("add object back", () => playfield.Add(hitObjectWithNested)); + + AddStep("skip to long past object", () => clock.CurrentTime = 100_000); + // the parent entry should still be linked to nested entries of pooled objects that are managed externally + // but not contain synthetic entries that were created for the non-pooled objects. + AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1)); + } + private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) { AddStep("create test", () => @@ -289,7 +336,7 @@ namespace osu.Game.Tests.Visual.Gameplay RegisterPool(poolSize); RegisterPool(poolSize); RegisterPool(poolSize); - RegisterPool(poolSize); + RegisterPool(poolSize); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); @@ -422,16 +469,22 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestHitObjectWithNested : TestHitObject { + public IEnumerable NestedObjects { get; init; } = Array.Empty(); + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - for (int i = 0; i < 3; ++i) - AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 }); + foreach (var ho in NestedObjects) + AddNested(ho); } } - private class NestedHitObject : ConvertHitObject + private class PooledNestedHitObject : ConvertHitObject + { + } + + private class NonPooledNestedHitObject : ConvertHitObject { } @@ -482,6 +535,9 @@ namespace osu.Game.Tests.Visual.Gameplay nestedContainer.Clear(false); } + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + => hitObject is NonPooledNestedHitObject nonPooled ? new DrawableNestedHitObject(nonPooled) : null; + protected override void CheckForResult(bool userTriggered, double timeOffset) { base.CheckForResult(userTriggered, timeOffset); @@ -490,25 +546,30 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private partial class DrawableNestedHitObject : DrawableHitObject + private partial class DrawableNestedHitObject : DrawableHitObject { public DrawableNestedHitObject() - : this(null) { } - public DrawableNestedHitObject(NestedHitObject hitObject) + public DrawableNestedHitObject(PooledNestedHitObject hitObject) + : base(hitObject) + { + } + + public DrawableNestedHitObject(NonPooledNestedHitObject hitObject) : base(hitObject) { - Size = new Vector2(15); - Colour = Colour4.White; - RelativePositionAxes = Axes.Both; - Origin = Anchor.Centre; } [BackgroundDependencyLoader] private void load() { + Size = new Vector2(15); + Colour = Colour4.White; + RelativePositionAxes = Axes.Both; + Origin = Anchor.Centre; + AddInternal(new Circle { RelativeSizeAxes = Axes.Both, From bae7670855a9060d9eec38628039ceca573a6feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:33:11 +0200 Subject: [PATCH 48/60] Redirect `HitObjectEntryManager` child mapping to HOLE --- .../Objects/HitObjectLifetimeEntry.cs | 2 +- .../Objects/Pooling/HitObjectEntryManager.cs | 35 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index 4e058e7c31..69a78c6bd0 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Objects /// /// The list of for the 's nested objects (if any). /// - public readonly List NestedEntries = new List(); + public List NestedEntries { get; internal set; } = new List(); /// /// The result that was judged with. diff --git a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs index 6c39ea44da..08f693bae3 100644 --- a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs +++ b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs @@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Objects.Pooling /// private readonly Dictionary parentMap = new Dictionary(); - /// - /// Stores the list of child entries for each hit object managed by this . - /// - private readonly Dictionary> childrenMap = new Dictionary>(); - public void Add(HitObjectLifetimeEntry entry, HitObject? parent) { HitObject hitObject = entry.HitObject; @@ -57,14 +52,13 @@ namespace osu.Game.Rulesets.Objects.Pooling // Add the entry. entryMap[hitObject] = entry; - childrenMap[hitObject] = new List(); // If the entry has a parent, set it and add the entry to the parent's children. if (parent != null) { parentMap[entry] = parent; - if (childrenMap.TryGetValue(parent, out var parentChildEntries)) - parentChildEntries.Add(entry); + if (entryMap.TryGetValue(parent, out var parentEntry)) + parentEntry.NestedEntries.Add(entry); } hitObject.DefaultsApplied += onDefaultsApplied; @@ -81,15 +75,12 @@ namespace osu.Game.Rulesets.Objects.Pooling entryMap.Remove(hitObject); // If the entry has a parent, unset it and remove the entry from the parents' children. - if (parentMap.Remove(entry, out var parent) && childrenMap.TryGetValue(parent, out var parentChildEntries)) - parentChildEntries.Remove(entry); + if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry)) + parentEntry.NestedEntries.Remove(entry); // Remove all the entries' children. - if (childrenMap.Remove(hitObject, out var childEntries)) - { - foreach (var childEntry in childEntries) - Remove(childEntry); - } + foreach (var childEntry in entry.NestedEntries) + Remove(childEntry); hitObject.DefaultsApplied -= onDefaultsApplied; OnEntryRemoved?.Invoke(entry, parent); @@ -105,16 +96,16 @@ namespace osu.Game.Rulesets.Objects.Pooling /// private void onDefaultsApplied(HitObject hitObject) { - if (!childrenMap.Remove(hitObject, out var childEntries)) + if (!entryMap.TryGetValue(hitObject, out var entry)) return; - // Remove all the entries' children. At this point the parents' (this entries') children list has been removed from the map, so this does not cause upwards traversal. - foreach (var entry in childEntries) - Remove(entry); + // Replace the entire list rather than clearing to prevent circular traversal later. + var previousEntries = entry.NestedEntries; + entry.NestedEntries = new List(); - // The removed children list needs to be added back to the map for the entry to potentially receive children. - childEntries.Clear(); - childrenMap[hitObject] = childEntries; + // Remove all the entries' children. At this point the parents' (this entries') children list has been reconstructed, so this does not cause upwards traversal. + foreach (var nested in previousEntries) + Remove(nested); } } } From 0ceaf3c451e0135d529d7ddba9b8e55069d62031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:39:39 +0200 Subject: [PATCH 49/60] Ensure synthetic entries from non-pooled DHO are linked to parents --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 12 ++++++++++++ .../Objects/Pooling/HitObjectEntryManager.cs | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 07c0d1f8a1..41d46fe85d 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -218,6 +218,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected sealed override void OnApply(HitObjectLifetimeEntry entry) { + Debug.Assert(Entry != null); + // LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset. // We override this with DHO's InitialLifetimeOffset for a non-pooled DHO. if (entry is SyntheticHitObjectEntry) @@ -247,6 +249,12 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.ParentHitObject = this; nestedHitObjects.Add(drawableNested); + + // assume that synthetic entries are not pooled and therefore need to be managed from within the DHO. + // this is important for the correctness of value of flags such as `AllJudged`. + if (drawableNested.Entry is SyntheticHitObjectEntry syntheticNestedEntry) + Entry.NestedEntries.Add(syntheticNestedEntry); + AddNestedHitObject(drawableNested); } @@ -290,6 +298,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected sealed override void OnFree(HitObjectLifetimeEntry entry) { + Debug.Assert(Entry != null); + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) @@ -318,6 +328,8 @@ namespace osu.Game.Rulesets.Objects.Drawables } nestedHitObjects.Clear(); + // clean up synthetic entries manually added in `Apply()`. + Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry); ClearNestedHitObjects(); HitObject.DefaultsApplied -= onDefaultsApplied; diff --git a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs index 08f693bae3..fabf4fc444 100644 --- a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs +++ b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs @@ -65,8 +65,11 @@ namespace osu.Game.Rulesets.Objects.Pooling OnEntryAdded?.Invoke(entry, parent); } - public void Remove(HitObjectLifetimeEntry entry) + public bool Remove(HitObjectLifetimeEntry entry) { + if (entry is SyntheticHitObjectEntry) + return false; + HitObject hitObject = entry.HitObject; if (!entryMap.ContainsKey(hitObject)) @@ -84,6 +87,7 @@ namespace osu.Game.Rulesets.Objects.Pooling hitObject.DefaultsApplied -= onDefaultsApplied; OnEntryRemoved?.Invoke(entry, parent); + return true; } public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry) From 6c4e52821de1214a0c8aa75e47f5d7c1fe64f6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jul 2023 22:42:50 +0200 Subject: [PATCH 50/60] Redirect judgement-related flags from DHO to HOLE --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 10 +++++----- osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 41d46fe85d..08dcf91e52 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Objects.Drawables public virtual bool DisplayResult => true; /// - /// Whether this and all of its nested s have been judged. + /// The scoring result of this . /// - public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged); + public JudgementResult Result => Entry?.Result; /// /// Whether this has been hit. This occurs if is hit. @@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Whether this has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Result?.HasResult ?? true; + public bool Judged => Entry?.Judged ?? true; /// - /// The scoring result of this . + /// Whether this and all of its nested s have been judged. /// - public JudgementResult Result => Entry?.Result; + public bool AllJudged => Entry?.AllJudged ?? true; /// /// The relative X position of this hit object for sample playback balance adjustment. diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index 69a78c6bd0..1d99e7440f 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; @@ -31,6 +32,17 @@ namespace osu.Game.Rulesets.Objects /// internal JudgementResult? Result; + /// + /// Whether has been judged. + /// Note: This does NOT include nested hitobjects. + /// + public bool Judged => Result?.HasResult ?? true; + + /// + /// Whether and all of its nested objects have been judged. + /// + public bool AllJudged => Judged && NestedEntries.All(h => h.AllJudged); + private readonly IBindable startTimeBindable = new BindableDouble(); internal event Action? RevertResult; From b0f6b22fa7872b4eab09b6782af2297568ec17ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 23:48:46 +0200 Subject: [PATCH 51/60] Add assertions covering correctness of judged flags on entry --- osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index e3afb91040..fea7456472 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -242,18 +242,23 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2); AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1); AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested)); AddStep("clear judged", () => playfield.JudgedObjects.Clear()); + AddStep("add object back", () => playfield.Add(hitObjectWithNested)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); AddStep("skip to long past object", () => clock.CurrentTime = 100_000); // the parent entry should still be linked to nested entries of pooled objects that are managed externally // but not contain synthetic entries that were created for the non-pooled objects. AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1)); + AddAssert("entry all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.True); } private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) From 3f8dfc7cb035adc38e849ac1fc895739aec86b6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:02:03 +0900 Subject: [PATCH 52/60] Fix fallback for `Judged` to be more correct Without this change, when the `Judged` value is checked on an `HitObjectLifetimeEntry` it would return `true` if a `DrawableHitObject` has not yet been associated with the entry. Which is completely wrong. Of note, the usage in `DrawableHitObject` will have never fallen through to this incorrect value as they always have a result populated: https://github.com/ppy/osu/blob/f26f001e1d01ca6bb53225da7bf06c0ad21153c5/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs#L721-L726 --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 08dcf91e52..e4d8eb2335 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Whether this has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Entry?.Judged ?? true; + public bool Judged => Entry?.Judged ?? false; /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => Entry?.AllJudged ?? true; + public bool AllJudged => Entry?.AllJudged ?? false; /// /// The relative X position of this hit object for sample playback balance adjustment. diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index 1d99e7440f..4450f026b4 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Objects /// Whether has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Result?.HasResult ?? true; + public bool Judged => Result?.HasResult ?? false; /// /// Whether and all of its nested objects have been judged. From e21dc56fcbc307b517ee51491d06a29cc8f07e9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:20:25 +0900 Subject: [PATCH 53/60] Add test coverage of `Judged` state --- .../Gameplay/TestSceneDrawableHitObject.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 04fc4cafbd..10dbede2e0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -80,7 +81,9 @@ namespace osu.Game.Tests.Gameplay { TestLifetimeEntry entry = null; AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 }); + assertJudged(() => entry, false); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); + assertJudged(() => entry, false); AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET); TestDrawableHitObject dho = null; @@ -91,6 +94,7 @@ namespace osu.Game.Tests.Gameplay }); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY); + assertJudged(() => entry, false); } [Test] @@ -138,6 +142,29 @@ namespace osu.Game.Tests.Gameplay AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss); } + [Test] + public void TestJudgedStateThroughLifetime() + { + TestDrawableHitObject dho = null; + HitObjectLifetimeEntry lifetimeEntry = null; + + AddStep("Create lifetime entry", () => lifetimeEntry = new HitObjectLifetimeEntry(new HitObject { StartTime = Time.Current })); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Create DHO and apply entry", () => + { + Child = dho = new TestDrawableHitObject(); + dho.Apply(lifetimeEntry); + }); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Apply result", () => dho.MissForcefully()); + + assertJudged(() => lifetimeEntry, true); + } + [Test] public void TestResultSetBeforeLoadComplete() { @@ -154,15 +181,20 @@ namespace osu.Game.Tests.Gameplay } }; }); + assertJudged(() => lifetimeEntry, true); AddStep("Create DHO and apply entry", () => { dho = new TestDrawableHitObject(); dho.Apply(lifetimeEntry); Child = dho; }); + assertJudged(() => lifetimeEntry, true); AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit)); } + private void assertJudged(Func entry, bool val) => + AddAssert(val ? "Is judged" : "Not judged", () => entry().Judged, () => Is.EqualTo(val)); + private partial class TestDrawableHitObject : DrawableHitObject { public const double INITIAL_LIFETIME_OFFSET = 100; From 9a7bf1beddecdcdb5281f2a1cf62be26f1f513bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:44:01 +0900 Subject: [PATCH 54/60] Fix reversed order of sample return --- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index a04c4b60f2..6098db4f7a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -28,8 +28,8 @@ namespace osu.Game.Rulesets.Taiko.UI { PlaySamples(new ISampleInfo[] { - hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), - baseSample + baseSample, + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH) }); } else From 00c68cad532d5898f5d6135d6df202db00caf9f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 19:47:44 +0900 Subject: [PATCH 55/60] Fix new scoring related properties not storing to realm due to `internal` spec --- osu.Game/Scoring/ScoreInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index eddd1bb80a..ea9007f4dc 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -64,7 +64,7 @@ namespace osu.Game.Scoring /// /// This may not match the version stored in the replay files. /// - internal int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION; + public int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION; /// /// Used to preserve the total score for legacy scores. @@ -72,7 +72,7 @@ namespace osu.Game.Scoring /// /// Not populated if is false. /// - internal long? LegacyTotalScore { get; set; } + public long? LegacyTotalScore { get; set; } public int MaxCombo { get; set; } From fbab5acac100211f0cffe6bdf7182eff6297baf1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 00:46:09 +0900 Subject: [PATCH 56/60] Remove not-yet-implemented settings group comments --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index b6b385e262..2fd2ed30f6 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -37,8 +37,6 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(0, 20), Children = new PlayerSettingsGroup[] { - //CollectionSettings = new CollectionSettings(), - //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, VisualSettings = new VisualSettings { Expanded = { Value = false } }, new AudioSettings { Expanded = { Value = false } } From 95a9b532dfc8feff57e524a2361798a6968ff2e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 00:46:28 +0900 Subject: [PATCH 57/60] Remember state of replay settings visibility --- osu.Game/Configuration/OsuConfigManager.cs | 4 +- .../Localisation/GameplaySettingsStrings.cs | 9 +++- .../Settings/Sections/Gameplay/HUDSettings.cs | 14 ++++-- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 25 ---------- osu.Game/Screens/Play/HUDOverlay.cs | 47 ++++++++++--------- 5 files changed, 45 insertions(+), 54 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index ba555a7926..edcbb94368 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -129,6 +129,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); + SetDefault(OsuSetting.ReplaySettingsOverlay, true); SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); @@ -382,6 +383,7 @@ namespace osu.Game.Configuration SafeAreaConsiderations, ComboColourNormalisationAmount, ProfileCoverExpanded, - EditorLimitedDistanceSnap + EditorLimitedDistanceSnap, + ReplaySettingsOverlay } } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 40f39d927d..f52f6abb89 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -65,10 +65,15 @@ namespace osu.Game.Localisation public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode"); /// - /// "Show health display even when you can't fail" + /// "Show health display even when you can't fail" /// public static LocalisableString ShowHealthDisplayWhenCantFail => new TranslatableString(getKey(@"show_health_display_when_cant_fail"), @"Show health display even when you can't fail"); + /// + /// "Show replay settings overlay" + /// + public static LocalisableString ShowReplaySettingsOverlay => new TranslatableString(getKey(@"show_replay_settings_overlay"), @"Show replay settings overlay"); + /// /// "Fade playfield to red when health is low" /// @@ -134,6 +139,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index e7c83159cd..3e67b2f103 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -25,10 +25,9 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { - ClassicDefault = false, - LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, - Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), - Keywords = new[] { "hp", "bar" } + LabelText = GameplaySettingsStrings.ShowReplaySettingsOverlay, + Current = config.GetBindable(OsuSetting.ReplaySettingsOverlay), + Keywords = new[] { "hide" }, }, new SettingsCheckbox { @@ -41,6 +40,13 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard, Current = config.GetBindable(OsuSetting.GameplayLeaderboard), }, + new SettingsCheckbox + { + ClassicDefault = false, + LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Keywords = new[] { "hp", "bar" } + }, }; } } diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 2fd2ed30f6..dbb0456cd0 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -3,10 +3,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osuTK; using osu.Game.Screens.Play.PlayerSettings; -using osuTK.Input; namespace osu.Game.Screens.Play.HUD { @@ -14,16 +12,12 @@ namespace osu.Game.Screens.Play.HUD { private const int fade_duration = 200; - public bool ReplayLoaded; - public readonly PlaybackSettings PlaybackSettings; public readonly VisualSettings VisualSettings; public PlayerSettingsOverlay() { - AlwaysPresent = true; - Anchor = Anchor.TopRight; Origin = Anchor.TopRight; AutoSizeAxes = Axes.Both; @@ -46,24 +40,5 @@ namespace osu.Game.Screens.Play.HUD protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); - - // We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible - public override bool PropagateNonPositionalInputSubTree => true; - - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) return false; - - if (e.ControlPressed) - { - if (e.Key == Key.H && ReplayLoaded) - { - ToggleVisibility(); - return true; - } - } - - return base.OnKeyDown(e); - } } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f0a2975958..55843ec17c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -28,6 +28,7 @@ using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; +using MarginPadding = osu.Framework.Graphics.MarginPadding; namespace osu.Game.Screens.Play { @@ -78,6 +79,7 @@ namespace osu.Game.Screens.Play public Bindable ShowHud { get; } = new BindableBool(); private Bindable configVisibilityMode; + private Bindable configSettingsOverlay; private readonly BindableBool replayLoaded = new BindableBool(); @@ -178,6 +180,7 @@ namespace osu.Game.Screens.Play ModDisplay.Current.Value = mods; configVisibilityMode = config.GetBindable(OsuSetting.HUDVisibilityMode); + configSettingsOverlay = config.GetBindable(OsuSetting.ReplaySettingsOverlay); if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce) { @@ -204,9 +207,24 @@ namespace osu.Game.Screens.Play holdingForHUD.BindValueChanged(_ => updateVisibility()); IsPlaying.BindValueChanged(_ => updateVisibility()); - configVisibilityMode.BindValueChanged(_ => updateVisibility(), true); + configVisibilityMode.BindValueChanged(_ => updateVisibility()); + configSettingsOverlay.BindValueChanged(_ => updateVisibility()); - replayLoaded.BindValueChanged(replayLoadedValueChanged, true); + replayLoaded.BindValueChanged(e => + { + if (e.NewValue) + { + ModDisplay.FadeIn(200); + InputCountController.Margin = new MarginPadding(10) { Bottom = 30 }; + } + else + { + ModDisplay.Delay(2000).FadeOut(200); + InputCountController.Margin = new MarginPadding(10); + } + + updateVisibility(); + }, true); } protected override void Update() @@ -280,6 +298,11 @@ namespace osu.Game.Screens.Play return; } + if (configSettingsOverlay.Value && replayLoaded.Value) + PlayerSettingsOverlay.Show(); + else + PlayerSettingsOverlay.Hide(); + switch (configVisibilityMode.Value) { case HUDVisibilityMode.Never: @@ -297,26 +320,6 @@ namespace osu.Game.Screens.Play } } - private void replayLoadedValueChanged(ValueChangedEvent e) - { - PlayerSettingsOverlay.ReplayLoaded = e.NewValue; - - if (e.NewValue) - { - PlayerSettingsOverlay.Show(); - ModDisplay.FadeIn(200); - InputCountController.Margin = new MarginPadding(10) { Bottom = 30 }; - } - else - { - PlayerSettingsOverlay.Hide(); - ModDisplay.Delay(2000).FadeOut(200); - InputCountController.Margin = new MarginPadding(10); - } - - updateVisibility(); - } - protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { if (drawableRuleset is ICanAttachHUDPieces attachTarget) From 929189530529ec3129e84b838f84bdf6e07441ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 01:00:41 +0900 Subject: [PATCH 58/60] Make key for toggling replay settings customisable --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 5 +++++ osu.Game/Screens/Play/HUDOverlay.cs | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 64268c73d0..c2d08ffff8 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -116,6 +116,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), + new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), @@ -374,5 +375,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))] ExportReplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))] + ToggleReplaySettings, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 9e53b23180..f93d86225c 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -324,6 +324,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus"); + /// + /// "Toggle replay settings" + /// + public static LocalisableString ToggleReplaySettings => new TranslatableString(getKey(@"toggle_replay_settings"), @"Toggle replay settings"); + /// /// "Save replay" /// diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 55843ec17c..d8d4daf143 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -357,6 +357,10 @@ namespace osu.Game.Screens.Play switch (e.Action) { + case GlobalAction.ToggleReplaySettings: + configSettingsOverlay.Value = !configSettingsOverlay.Value; + return true; + case GlobalAction.HoldForHUD: holdingForHUD.Value = true; return true; From cdb8a56df40b83c40f243d2dab9df2a531139084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Jul 2023 22:41:20 +0200 Subject: [PATCH 59/60] Remove weird aliased using Doesn't appear to be required. --- osu.Game/Screens/Play/HUDOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index d8d4daf143..d11171e3fe 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -28,7 +28,6 @@ using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; -using MarginPadding = osu.Framework.Graphics.MarginPadding; namespace osu.Game.Screens.Play { From 1938bdbf9d42a69d6a718b8022fb886eaf78a554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Jul 2023 22:45:10 +0200 Subject: [PATCH 60/60] Move replay settings toggle to replay key bindings section --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index c2d08ffff8..01c454e3f9 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -116,7 +116,6 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), - new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), @@ -130,6 +129,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), + new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), }; public IEnumerable SongSelectKeyBindings => new[]