From 02111e38542edaa0657c9e1ea7ac48ed4b0d10f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 1 Jun 2023 13:22:37 +0900 Subject: [PATCH 001/555] 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 002/555] 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 003/555] "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 004/555] 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 005/555] 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 006/555] 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 007/555] 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 008/555] 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 009/555] 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 ccf6ed1e5b58f8b6810f589969d752f4681d3fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 18:10:49 +0200 Subject: [PATCH 010/555] Add failing test cases --- .../TestSceneMaximumScore.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs new file mode 100644 index 0000000000..3d0abaceb5 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs @@ -0,0 +1,147 @@ +// 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 NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneMaximumScore : RateAdjustedBeatmapTestScene + { + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private List judgementResults = new List(); + + [Test] + public void TestSimultaneousTickAndNote() + { + performTest( + new List + { + new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 0, + }, + new Note + { + StartTime = 2000, + Column = 1 + } + }, + new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2), + new ManiaReplayFrame(2001, ManiaAction.Key1), + new ManiaReplayFrame(3000) + }); + + AddAssert("all objects perfectly judged", + () => judgementResults.Select(result => result.Type), + () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); + AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + } + + [Test] + public void TestSimultaneousLongNotes() + { + performTest( + new List + { + new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 0, + }, + new HoldNote + { + StartTime = 2000, + Duration = 2000, + Column = 1 + } + }, + new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2), + new ManiaReplayFrame(3000, ManiaAction.Key2), + new ManiaReplayFrame(4000) + }); + + AddAssert("all objects perfectly judged", + () => judgementResults.Select(result => result.Type), + () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); + AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + } + + private void performTest(List hitObjects, List frames) + { + var beatmap = new Beatmap + { + HitObjects = hitObjects, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} From 04e812b5ab6f2098e548ca8f292053a2f84d2704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:08:14 +0200 Subject: [PATCH 011/555] Make `JudgementProcessor.SimulateAutoplay()` non-virtual Nobody overrides this, and with the structure given, overriders would have to rewrite half of this code anyway. The fact that the class has 2 other overridable members (`CreateResult()`, `GetSimulatedHitResult()`) which cease to have any meaning if `SimulateAutoplay()` is overridden also contributes to taking this decision. --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index b16c307206..181fbef405 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring /// /// This provided temporarily. DO NOT USE. /// The to simulate. - protected virtual void SimulateAutoplay(IBeatmap beatmap) + protected void SimulateAutoplay(IBeatmap beatmap) { IsSimulating = true; From 3295294cbc4cc324dd8281c8ed1258ea7eeed0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:10:34 +0200 Subject: [PATCH 012/555] Reorder autoplay-related virtual methods closer together --- .../Rulesets/Scoring/JudgementProcessor.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 181fbef405..9c86cbfe90 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -137,13 +137,6 @@ namespace osu.Game.Rulesets.Scoring JudgedHits += count; } - /// - /// Creates the that represents the scoring result for a . - /// - /// The which was judged. - /// The that provides the scoring information. - protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); - /// /// Simulates an autoplay of the to determine scoring values. /// @@ -174,6 +167,20 @@ namespace osu.Game.Rulesets.Scoring IsSimulating = false; } + /// + /// Creates the that represents the scoring result for a . + /// + /// The which was judged. + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); + + /// + /// Gets a simulated for a judgement. Used during to simulate a "perfect" play. + /// + /// The judgement to simulate a for. + /// The simulated for the judgement. + protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; + protected override void Update() { base.Update(); @@ -184,12 +191,5 @@ namespace osu.Game.Rulesets.Scoring // Last applied result is guaranteed to be non-null when JudgedHits > 0. || lastAppliedResult.AsNonNull().TimeAbsolute < Clock.CurrentTime); } - - /// - /// Gets a simulated for a judgement. Used during to simulate a "perfect" play. - /// - /// The judgement to simulate a for. - /// The simulated for the judgement. - protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; } } From 462570801a1441580af32e049400c2f682fb876d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:15:43 +0200 Subject: [PATCH 013/555] Introduce new method for enumeration of objects during autoplay simulation --- .../Rulesets/Scoring/JudgementProcessor.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 9c86cbfe90..2c42a08864 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.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.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; @@ -146,14 +147,11 @@ namespace osu.Game.Rulesets.Scoring { IsSimulating = true; - foreach (var obj in beatmap.HitObjects) + foreach (var obj in EnumerateHitObjects(beatmap)) simulate(obj); void simulate(HitObject obj) { - foreach (var nested in obj.NestedHitObjects) - simulate(nested); - var judgement = obj.CreateJudgement(); var result = CreateResult(obj, judgement); @@ -167,6 +165,29 @@ namespace osu.Game.Rulesets.Scoring IsSimulating = false; } + /// + /// Enumerates all s in the given in the order in which they are to be judged. + /// Used in . + /// + /// + /// In Score V2, the score awarded for each object includes a component based on the combo value after the judgement of that object. + /// This means that the score is dependent on the order of evaluation of judgements. + /// This method is provided so that rulesets can specify custom ordering that is correct for them and matches processing order during actual gameplay. + /// + protected virtual IEnumerable EnumerateHitObjects(IBeatmap beatmap) + => enumerateRecursively(beatmap.HitObjects); + + private IEnumerable enumerateRecursively(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + foreach (var nested in enumerateRecursively(hitObject.NestedHitObjects)) + yield return nested; + + yield return hitObject; + } + } + /// /// Creates the that represents the scoring result for a . /// From e46b4209c3efecf10143bf2b7ebb4a4b6bbe821d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:35:49 +0200 Subject: [PATCH 014/555] Remove no-longer-needed local method --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 2c42a08864..e9f3bcb949 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -148,9 +148,6 @@ namespace osu.Game.Rulesets.Scoring IsSimulating = true; foreach (var obj in EnumerateHitObjects(beatmap)) - simulate(obj); - - void simulate(HitObject obj) { var judgement = obj.CreateJudgement(); From 0065334241f77b703c1c7523ab0e917a4ae53fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Jun 2023 20:34:06 +0200 Subject: [PATCH 015/555] Fix mania autoplay simulation judging objects in different order to gameplay --- .../Scoring/ManiaScoreProcessor.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 6292ed75cd..a0f6ac572d 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -2,7 +2,12 @@ // 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.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring @@ -16,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Scoring { } + protected override IEnumerable EnumerateHitObjects(IBeatmap beatmap) + => base.EnumerateHitObjects(beatmap).OrderBy(ho => (ManiaHitObject)ho, JudgementOrderComparer.DEFAULT); + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { return 10000 * comboProgress @@ -25,5 +33,27 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)); + + private class JudgementOrderComparer : IComparer + { + public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer(); + + public int Compare(ManiaHitObject? x, ManiaHitObject? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(x, null)) return -1; + if (ReferenceEquals(y, null)) return 1; + + int result = x.GetEndTime().CompareTo(y.GetEndTime()); + if (result != 0) + return result; + + // due to the way input is handled in mania, notes take precedence over ticks in judging order. + if (x is Note && y is not Note) return -1; + if (x is not Note && y is Note) return 1; + + return x.Column.CompareTo(y.Column); + } + } } } From 975e9baf432dcb2af21869ad5f5e7fa47bfce34d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 15 Jun 2023 19:55:51 +0900 Subject: [PATCH 016/555] 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 017/555] 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 018/555] 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 019/555] 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 020/555] 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 021/555] 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 022/555] 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 023/555] 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 024/555] 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 025/555] 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 026/555] 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 027/555] 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 028/555] 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 029/555] 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 030/555] 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 031/555] 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 032/555] 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 033/555] 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 034/555] 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 035/555] 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 036/555] 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 037/555] 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 038/555] 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 e505e71d07ae4109ad66de2e6fee801bd217cc16 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:40:00 +0200 Subject: [PATCH 039/555] Merge the two `app.manifest` files --- app.manifest | 1 + osu.Desktop/app.manifest | 21 --------------------- osu.Desktop/osu.Desktop.csproj | 1 - 3 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 osu.Desktop/app.manifest diff --git a/app.manifest b/app.manifest index 533c6ff208..b85df82c4d 100644 --- a/app.manifest +++ b/app.manifest @@ -1,6 +1,7 @@  + 1 diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest deleted file mode 100644 index a11cee132c..0000000000 --- a/osu.Desktop/app.manifest +++ /dev/null @@ -1,21 +0,0 @@ - - - - 1 - - - - - - - - - - - - - - true - - - diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index f1b9c92429..16d6a81d40 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -8,7 +8,6 @@ osu! osu!(lazer) lazer.ico - app.manifest 0.0.0 0.0.0 From 909edefa2048f59ffb6cf623f8fce8d16f5ca2d2 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:41:18 +0200 Subject: [PATCH 040/555] Remove unnecessary `` osu! was working fine without this. --- app.manifest | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app.manifest b/app.manifest index b85df82c4d..2e5ba1b18a 100644 --- a/app.manifest +++ b/app.manifest @@ -32,16 +32,4 @@ true - - - - - From bfa5bcb2a77de4b3416128b8d829e99fee06036d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:43:01 +0200 Subject: [PATCH 041/555] Update `` to match what osu! actually supports --- app.manifest | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app.manifest b/app.manifest index 2e5ba1b18a..69702111ce 100644 --- a/app.manifest +++ b/app.manifest @@ -15,15 +15,9 @@ - - - - - - - + From b0e716feab3f728e4bf59b3800a86fdaf1e030ee Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 30 Jun 2023 21:59:46 +0200 Subject: [PATCH 042/555] Use correct `perMonitorV2` `` Yes, `xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"` is required else the game will exit with code 500 on startup. --- app.manifest | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.manifest b/app.manifest index 69702111ce..ad8b5d005a 100644 --- a/app.manifest +++ b/app.manifest @@ -23,7 +23,8 @@ - true + per Monitor + perMonitorV2 From e38ac4185cdd630a105452d19c6bdf627e794621 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 1 Jul 2023 19:02:09 +0200 Subject: [PATCH 043/555] Update inline with framework `IWindow` changes --- osu.Desktop/OsuGameDesktop.cs | 8 +++----- osu.Game/OsuGame.cs | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index efd3d358b7..a0db896f46 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -147,14 +147,12 @@ namespace osu.Desktop { base.SetHost(host); - var desktopWindow = (SDL2DesktopWindow)host.Window; - var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"); if (iconStream != null) - desktopWindow.SetIconFromStream(iconStream); + host.Window.SetIconFromStream(iconStream); - desktopWindow.CursorState |= CursorState.Hidden; - desktopWindow.Title = Name; + host.Window.CursorState |= CursorState.Hidden; + host.Window.Title = Name; } protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 93dd97ea15..5b654e0c16 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -289,9 +289,9 @@ namespace osu.Game { base.SetHost(host); - if (host.Window is SDL2Window sdlWindow) + if (host.Window != null) { - sdlWindow.DragDrop += path => + host.Window.DragDrop += path => { // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) From caba571263b6fca21fde60d25ac755a8308e21ec Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 1 Jul 2023 19:11:48 +0200 Subject: [PATCH 044/555] Remove manifest DPI awareness entires It'll be properly handled by osu!framework with https://github.com/ppy/osu/pull/24092 --- app.manifest | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app.manifest b/app.manifest index ad8b5d005a..088ad1dde7 100644 --- a/app.manifest +++ b/app.manifest @@ -21,10 +21,4 @@ - - - per Monitor - perMonitorV2 - - From 67650831bd07fe8756c5595d9117ca04064b9439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:19:25 +0900 Subject: [PATCH 045/555] 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 d74b1e148dd0afae80603afb2b16e68e1c3640a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:50:34 +0900 Subject: [PATCH 046/555] Make `ScoreInfo.BeatmapInfo` nullable --- osu.Desktop/DiscordRichPresence.cs | 2 +- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- osu.Game.Tests/Models/DisplayStringTest.cs | 8 ++++---- .../Ranking/TestSceneContractedPanelMiddleContent.cs | 2 +- osu.Game/Database/RealmAccess.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- osu.Game/Scoring/IScoreInfo.cs | 2 +- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 4 +++- osu.Game/Scoring/ScoreInfo.cs | 6 ++---- osu.Game/Scoring/ScoreInfoExtensions.cs | 2 +- osu.Game/Scoring/ScorePerformanceCache.cs | 2 +- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- osu.Game/Screens/Select/LocalScoreDeleteDialog.cs | 7 +------ osu.Game/Users/UserActivity.cs | 2 +- 15 files changed, 21 insertions(+), 26 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index fe3e08537e..b1e11d7a60 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -187,7 +187,7 @@ namespace osu.Desktop return edit.BeatmapInfo.ToString() ?? string.Empty; case UserActivity.WatchingReplay watching: - return watching.BeatmapInfo.ToString(); + return watching.BeatmapInfo?.ToString() ?? string.Empty; case UserActivity.InLobby lobby: return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index a193bacde5..ac4462c18b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; // TODO: The detection of rulesets is temporary until the leftover old skills have been reworked. - bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1; + bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1; double multiplier = 1.13; diff --git a/osu.Game.Tests/Models/DisplayStringTest.cs b/osu.Game.Tests/Models/DisplayStringTest.cs index d585a0eb9f..b5303e1dd6 100644 --- a/osu.Game.Tests/Models/DisplayStringTest.cs +++ b/osu.Game.Tests/Models/DisplayStringTest.cs @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Models var mock = new Mock(); mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary. - mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist"); - mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title"); - mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author"); - mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty"); + mock.Setup(m => m.Beatmap!.Metadata.Artist).Returns("artist"); + mock.Setup(m => m.Beatmap!.Metadata.Title).Returns("title"); + mock.Setup(m => m.Beatmap!.Metadata.Author.Username).Returns("author"); + mock.Setup(m => m.Beatmap!.DifficultyName).Returns("difficulty"); Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]")); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index 3004cb8a0c..0f17b08b7b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show excess mods score", () => { var score = TestResources.CreateTestScoreInfo(); - score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score); }); } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index da4caa42ba..f8b3f24a72 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -896,7 +896,7 @@ namespace osu.Game.Database var scores = migration.NewRealm.All(); foreach (var score in scores) - score.BeatmapHash = score.BeatmapInfo.Hash; + score.BeatmapHash = score.BeatmapInfo?.Hash ?? string.Empty; break; } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 89da8b9d32..14e137caf1 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -185,7 +185,7 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); currentState.State = SpectatedUserState.Playing; diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index 3644d099d9..d17558f800 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Scoring double? PP { get; } - IBeatmapInfo Beatmap { get; } + IBeatmapInfo? Beatmap { get; } IRulesetInfo Ruleset { get; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index f71da6c7e0..6cad8716b1 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -65,7 +65,7 @@ namespace osu.Game.Scoring.Legacy { sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID)); sw.Write(LATEST_VERSION); - sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); + sw.Write(score.ScoreInfo.BeatmapInfo!.MD5Hash); sw.Write(score.ScoreInfo.User.Username); sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}").ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 16658a598a..5770da10be 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -64,6 +64,8 @@ namespace osu.Game.Scoring protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { + Debug.Assert(model.BeatmapInfo != null); + // Ensure the beatmap is not detached. if (!model.BeatmapInfo.IsManaged) model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID); @@ -99,7 +101,7 @@ namespace osu.Game.Scoring if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) return; - var beatmap = score.BeatmapInfo.Detach(); + var beatmap = score.BeatmapInfo?.Detach(); var ruleset = score.Ruleset.Detach(); var rulesetInstance = ruleset.CreateInstance(); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6dfac419e5..6816e86e9d 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -36,7 +36,7 @@ namespace osu.Game.Scoring /// /// When setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. /// - public BeatmapInfo BeatmapInfo { get; set; } = null!; + public BeatmapInfo? BeatmapInfo { get; set; } /// /// The at the point in time when the score was set. @@ -129,14 +129,12 @@ namespace osu.Game.Scoring public int RankInt { get; set; } IRulesetInfo IScoreInfo.Ruleset => Ruleset; - IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; + IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages - public Guid BeatmapInfoID => BeatmapInfo.ID; - public int UserID => RealmUser.OnlineID; public int RulesetID => Ruleset.OnlineID; diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 85598076d6..6e57a9fd0b 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -13,7 +13,7 @@ namespace osu.Game.Scoring /// /// A user-presentable display title representing this score. /// - public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}"; + public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap?.GetDisplayTitle() ?? "unknown"}"; /// /// Orders an array of s by total score. diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index bdbcfe4efe..1f2b1aeb95 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, token).ConfigureAwait(false); + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes?.Attributes == null) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index c8920a734d..f187b8a302 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Ranking protected override APIRequest? FetchScores(Action>? scoresCallback) { - if (Score.BeatmapInfo.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index c4add31a4f..cd98872b65 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -4,9 +4,7 @@ using osu.Framework.Allocation; using osu.Game.Overlays.Dialog; using osu.Game.Scoring; -using System.Diagnostics; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; namespace osu.Game.Screens.Select { @@ -20,11 +18,8 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmapManager, ScoreManager scoreManager) + private void load(ScoreManager scoreManager) { - BeatmapInfo? beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - Debug.Assert(beatmapInfo != null); - BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; Icon = FontAwesome.Regular.TrashAlt; diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 1761282e2e..c82f642fdc 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -111,7 +111,7 @@ namespace osu.Game.Users protected string Username => score.User.Username; - public BeatmapInfo BeatmapInfo => score.BeatmapInfo; + public BeatmapInfo? BeatmapInfo => score.BeatmapInfo; public WatchingReplay(ScoreInfo score) { From f30dc59afec1c670e4c791026224a4c50862e6e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:50:50 +0900 Subject: [PATCH 047/555] Update tests to show expected score retention behaviour when saving a beatmap --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 10 +++++----- osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 84e6a6c00f..84d13ac85e 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -431,19 +431,18 @@ namespace osu.Game.Tests.Database await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); + Assert.That(imported.Beatmaps.First().Scores.Any()); + // imitate making local changes via editor // ReSharper disable once MethodHasAsyncOverload - realm.Write(_ => + realm.Write(r => { BeatmapInfo beatmap = imported.Beatmaps.First(); beatmap.Hash = "new_hash"; beatmap.ResetOnlineInfo(); }); - // for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap. - // the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (see: https://github.com/ppy/osu/pull/22539). - // TODO: revisit when fixing https://github.com/ppy/osu/issues/24069. - Assert.That(imported.Beatmaps.First().Scores.Any()); + Assert.That(!imported.Beatmaps.First().Scores.Any()); var importedSecondTime = await importer.Import(new ImportTask(temp)); @@ -461,6 +460,7 @@ namespace osu.Game.Tests.Database Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash); Assert.That(!importedFirstTimeBeatmap.Scores.Any()); Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1); + Assert.That(importedSecondTimeBeatmap.Scores.Single().BeatmapInfo, Is.EqualTo(importedSecondTimeBeatmap)); }); } diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 83cb54df3f..3934a67be0 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -387,10 +387,9 @@ namespace osu.Game.Tests.Database realm.Run(r => r.Refresh()); - // for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap. - // the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (https://github.com/ppy/osu/pull/22539). - // TODO: revisit when fixing https://github.com/ppy/osu/issues/24069. + // making changes to a beatmap doesn't remove the score from realm, but should disassociate the beatmap. checkCount(realm, 1); + Assert.That(realm.Run(r => r.All().First().BeatmapInfo), Is.Null); // reimport the original beatmap before local modifications var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value); From 64fc5e40e8377b4a38e3ef28f9e8544021485566 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 14:51:09 +0900 Subject: [PATCH 048/555] Move score attach logic to a helper method and call during editor save --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 1 + .../Database/BeatmapImporterUpdateTests.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 4 +--- osu.Game/Beatmaps/BeatmapInfo.cs | 16 ++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 3 +++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 84d13ac85e..1440f540b4 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -440,6 +440,7 @@ namespace osu.Game.Tests.Database BeatmapInfo beatmap = imported.Beatmaps.First(); beatmap.Hash = "new_hash"; beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); }); Assert.That(!imported.Beatmaps.First().Scores.Any()); diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 3934a67be0..e1bf8f5eae 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -383,6 +383,7 @@ namespace osu.Game.Tests.Database beatmapInfo.Hash = new_beatmap_hash; beatmapInfo.ResetOnlineInfo(); + beatmapInfo.UpdateLocalScores(s.Realm); }); realm.Run(r => r.Refresh()); diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index fd766490fc..d20027892f 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -20,7 +20,6 @@ using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Scoring; using Realms; namespace osu.Game.Beatmaps @@ -210,8 +209,7 @@ namespace osu.Game.Beatmaps // Let's reattach any matching scores that exist in the database, based on hash. foreach (BeatmapInfo beatmap in model.Beatmaps) { - foreach (var score in realm.All().Where(score => score.BeatmapHash == beatmap.Hash)) - score.BeatmapInfo = beatmap; + beatmap.UpdateLocalScores(realm); } ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst); diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 5019d64276..c1aeec1f71 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -234,6 +234,22 @@ namespace osu.Game.Beatmaps } } + /// + /// Local scores are retained separate from a beatmap's lifetime, matched via . + /// Therefore we need to detach / reattach scores when a beatmap is edited or imported. + /// + /// A realm instance in an active write transaction. + public void UpdateLocalScores(Realm realm) + { + // first disassociate any scores which are already attached and no longer valid. + foreach (var score in Scores) + score.BeatmapInfo = null; + + // then attach any scores which match the new hash. + foreach (var score in realm.All().Where(s => s.BeatmapHash == Hash)) + score.BeatmapInfo = this; + } + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 73811b2e62..295f6ed91a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -467,6 +467,9 @@ namespace osu.Game.Beatmaps if (transferCollections) beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); + liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) + .UpdateLocalScores(r); + // do not look up metadata. // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); From a0bed0fceccb370700b98c559572c7f4f5dbbd08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 15:18:51 +0900 Subject: [PATCH 049/555] Add full flow test of `UpdateLocalScores` --- .../Database/BeatmapImporterTests.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 1440f540b4..2766e4509e 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -417,6 +417,60 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestImport_Modify_Revert() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + var imported = await LoadOszIntoStore(importer, realm.Realm); + + await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); + + var score = realm.Run(r => r.All().Single()); + + string originalHash = imported.Beatmaps.First().Hash; + const string modified_hash = "new_hash"; + + Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score)); + + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First())); + + // imitate making local changes via editor + // ReSharper disable once MethodHasAsyncOverload + realm.Write(r => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = modified_hash; + beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); + }); + + Assert.That(!imported.Beatmaps.First().Scores.Any()); + + Assert.That(score.BeatmapInfo, Is.Null); + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + + // imitate making local changes via editor + // ReSharper disable once MethodHasAsyncOverload + realm.Write(r => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = originalHash; + beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); + }); + + Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score)); + + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First())); + }); + } + [Test] public void TestImport_ThenModifyMapWithScore_ThenImport() { From 1a6381bcbb26fc649a99a88cb07368ae0b249e7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jul 2023 15:35:09 +0900 Subject: [PATCH 050/555] 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 051/555] 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 052/555] `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 053/555] 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 054/555] 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 055/555] 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 056/555] 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 057/555] 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 058/555] 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 059/555] 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 060/555] 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 061/555] 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 a55809733dd1c9ec05f2a9ecfadcc97da827c17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 22:20:50 +0200 Subject: [PATCH 062/555] Expand `ScoreInfo.BeatmapInfo` xmldoc --- osu.Game/Scoring/ScoreInfo.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6816e86e9d..4798a105c5 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -34,7 +34,14 @@ namespace osu.Game.Scoring /// The this score was made against. /// /// - /// When setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. + /// + /// This property may be if the score was set on a beatmap (or a version of the beatmap) that is not available locally + /// e.g. due to online updates, or local modifications to the beatmap. + /// The property will only link to a if its matches . + /// + /// + /// Due to the above, whenever setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. + /// /// public BeatmapInfo? BeatmapInfo { get; set; } From bcdbdf57efe1218e2f378a2a4830425bf508a0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 22:22:57 +0200 Subject: [PATCH 063/555] Reword comment --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 2766e4509e..0eac70f9c8 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -454,7 +454,7 @@ namespace osu.Game.Tests.Database Assert.That(score.BeatmapInfo, Is.Null); Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); - // imitate making local changes via editor + // imitate reverting the local changes made above // ReSharper disable once MethodHasAsyncOverload realm.Write(r => { From e2ddcb23497468c45d1285797984fb531d0cef1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jul 2023 22:39:26 +0200 Subject: [PATCH 064/555] Silence a few remaining nullability warnings --- .../Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- .../Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs | 2 +- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index c05774400f..d71c72f4ec 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking var author = new RealmUser { Username = "mapper_name" }; var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author)); - score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); showPanel(score); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 42068ff117..c5b61c1a90 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -405,7 +405,7 @@ namespace osu.Game.Tests.Visual.Ranking public UnrankedSoloResultsScreen(ScoreInfo score) : base(score, true) { - Score.BeatmapInfo.OnlineID = 0; + Score.BeatmapInfo!.OnlineID = 0; Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending; } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 425f40258e..615a3e39af 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -163,7 +163,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, username, #pragma warning disable 618 - new StatisticText(score.MaxCombo, score.BeatmapInfo.MaxCombo, @"0\x"), + new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"), #pragma warning restore 618 }; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index e030b1e34f..c92b79cb4d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); - ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0; + ppColumn.Alpha = value.BeatmapInfo!.Status.GrantsPerformancePoints() ? 1 : 0; if (value.PP is double pp) ppColumn.Text = pp.ToLocalisableString(@"N0"); diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index dafdf00136..f7ae3eb62b 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play { IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo; - Debug.Assert(beatmap.OnlineID > 0); + Debug.Assert(beatmap!.OnlineID > 0); return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 82c429798e..d1dc1a81db 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Expanded [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { - var beatmap = score.BeatmapInfo; + var beatmap = score.BeatmapInfo!; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; 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 065/555] 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 066/555] 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 067/555] 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 068/555] 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 069/555] 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 070/555] 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 5947c2b298096e936eefb70943615153c20f5a87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 16:07:12 +0900 Subject: [PATCH 071/555] Throw if a null `BeatmapInfo` arrives during score import process --- osu.Game/Scoring/ScoreImporter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 5770da10be..5a52f72cd5 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -98,10 +98,12 @@ namespace osu.Game.Scoring /// The score to populate the statistics of. public void PopulateMaximumStatistics(ScoreInfo score) { + Debug.Assert(score.BeatmapInfo != null); + if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) return; - var beatmap = score.BeatmapInfo?.Detach(); + var beatmap = score.BeatmapInfo!.Detach(); var ruleset = score.Ruleset.Detach(); var rulesetInstance = ruleset.CreateInstance(); From 3b9d7af9ee284ec5269da4495eb9ed2e4dfb85fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 17:25:31 +0900 Subject: [PATCH 072/555] Fix taiko hit overlay animation timing not accounting for timing section start time --- .../Skinning/Legacy/LegacyCirclePiece.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 37eb95b86f..5516e025cd 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -8,6 +9,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; @@ -26,11 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Bindable currentCombo { get; } = new BindableInt(); private int animationFrame; - private double beatLength; // required for editor blueprints (not sure why these circle pieces are zero size). public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad; + private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT; + public LegacyCirclePiece() { RelativeSizeAxes = Axes.Both; @@ -39,11 +42,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [Resolved(canBeNull: true)] private GameplayState? gameplayState { get; set; } - [Resolved(canBeNull: true)] - private IBeatSyncProvider? beatSyncProvider { get; set; } - [BackgroundDependencyLoader] - private void load(ISkinSource skin, DrawableHitObject drawableHitObject) + private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider) { Drawable? getDrawableFor(string lookup) { @@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (foregroundLayer != null) AddInternal(foregroundLayer); + drawableHitObject.StartTimeBindable.BindValueChanged(startTime => + { + timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT; + }, true); + // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). // For now just stop at first frame for sanity. foreach (var c in InternalChildren) @@ -115,14 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return; } - if (beatSyncProvider?.ControlPoints != null) - { - beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength; - - animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1; - - animatableForegroundLayer.GotoFrame(animationFrame); - } + animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1; + animatableForegroundLayer.GotoFrame(animationFrame); } private Color4 accentColour; From 3f8dfc7cb035adc38e849ac1fc895739aec86b6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:02:03 +0900 Subject: [PATCH 073/555] 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 074/555] 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 075/555] 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 f54eb8d7fa7c3b97514afcee2fa7bb38f3a31b62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 15:07:07 +0900 Subject: [PATCH 076/555] Move `DrumSamplePlayer` to be a skinnable component --- .../Skinning/Argon/TaikoArgonSkinTransformer.cs | 3 +++ .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 3 +++ osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 3 ++- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 14 +++++++++----- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 5 ++++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 780018af4e..7e3b0e99b6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. return Drawable.Empty().With(d => d.Expire()); + case TaikoSkinComponents.DrumSamplePlayer: + return Drawable.Empty(); + case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionMiss: case TaikoSkinComponents.TaikoExplosionOk: diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index d61f9ac35d..894b91e9ce 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; + case TaikoSkinComponents.DrumSamplePlayer: + return null; + case TaikoSkinComponents.CentreHit: case TaikoSkinComponents.RimHit: if (hasHitCircle) diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index b8e3313e1b..28133ffcb2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionKiai, Scroller, Mascot, - KiaiGlow + KiaiGlow, + DrumSamplePlayer } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 6454fb5afa..f1dcd23698 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -12,13 +13,16 @@ namespace osu.Game.Rulesets.Taiko.UI { internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler { - private readonly DrumSampleTriggerSource leftRimSampleTriggerSource; - private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource; - private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource; - private readonly DrumSampleTriggerSource rightRimSampleTriggerSource; + private DrumSampleTriggerSource leftRimSampleTriggerSource = null!; + private DrumSampleTriggerSource leftCentreSampleTriggerSource = null!; + private DrumSampleTriggerSource rightCentreSampleTriggerSource = null!; + private DrumSampleTriggerSource rightRimSampleTriggerSource = null!; - public DrumSamplePlayer(HitObjectContainer hitObjectContainer) + [BackgroundDependencyLoader] + private void load(DrawableRuleset drawableRuleset) { + var hitObjectContainer = drawableRuleset.Playfield.HitObjectContainer; + InternalChildren = new Drawable[] { leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 9f9debe7d7..23ffac1f63 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -170,7 +170,10 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, }, drumRollHitContainer.CreateProxy(), - new DrumSamplePlayer(HitObjectContainer), + new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumSamplePlayer), _ => new DrumSamplePlayer()) + { + RelativeSizeAxes = Axes.Both, + }, // this is added at the end of the hierarchy to receive input before taiko objects. // but is proxied below everything to not cover visual effects such as hit explosions. inputDrum, From 6d4fa6569f7e189368d345605c5ddf9ab0b4f0d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:13:32 +0900 Subject: [PATCH 077/555] Add back required pieces to `GameplaySampleTriggerSource` from old PR --- .../UI/GameplaySampleTriggerSource.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index c554318357..efbf823058 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -34,14 +34,19 @@ namespace osu.Game.Rulesets.UI [Resolved] private IGameplayClock? gameplayClock { get; set; } + protected readonly AudioContainer AudioContainer; + public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; - InternalChild = hitSounds = new Container + InternalChild = AudioContainer = new AudioContainer { - Name = "concurrent sample pool", - ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound()) + Child = hitSounds = new Container + { + Name = "concurrent sample pool", + ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound()) + } }; } @@ -64,11 +69,22 @@ namespace osu.Game.Rulesets.UI protected virtual void PlaySamples(ISampleInfo[] samples) => Schedule(() => { - var hitSound = getNextSample(); - hitSound.Samples = samples; + var hitSound = GetNextSample(); + ApplySampleInfo(hitSound, samples); hitSound.Play(); }); + protected virtual void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples) + { + hitSound.Samples = samples; + } + + public void StopAllPlayback() => Schedule(() => + { + foreach (var sound in hitSounds) + sound.Stop(); + }); + protected HitObject? GetMostValidObject() { if (mostValidObject == null || isAlreadyHit(mostValidObject)) From ae86fc736a51d6e5c8d454ade8fe5a028664dc8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:03:11 +0900 Subject: [PATCH 078/555] Add argon-specific `DrumSamplePlayer` --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 49 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../UI/DrumSamplePlayer.cs | 13 +++-- .../UI/DrumSampleTriggerSource.cs | 2 +- 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs new file mode 100644 index 0000000000..5b690be780 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -0,0 +1,49 @@ +// 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.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonDrumSamplePlayer : DrumSamplePlayer + { + protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) => + new ArgonDrumSampleTriggerSource(hitObjectContainer); + + public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource + { + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer) + { + } + + public override void Play(HitType hitType) + { + // let the magic begin... + + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) + { + PlaySamples(new ISampleInfo[] + { + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), + baseSample + }); + } + else + { + PlaySamples(new ISampleInfo[] { baseSample }); + } + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 7e3b0e99b6..9fcecd2b1a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return Drawable.Empty().With(d => d.Expire()); case TaikoSkinComponents.DrumSamplePlayer: - return Drawable.Empty(); + return new ArgonDrumSamplePlayer(); case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionMiss: diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index f1dcd23698..b60ad209ff 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.UI { - internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler + public partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler { private DrumSampleTriggerSource leftRimSampleTriggerSource = null!; private DrumSampleTriggerSource leftCentreSampleTriggerSource = null!; @@ -25,13 +25,16 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChildren = new Drawable[] { - leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), + leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), }; } + protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) + => new DrumSampleTriggerSource(hitObjectContainer); + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 6098db4f7a..939eba684b 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.UI { } - public void Play(HitType hitType) + public virtual void Play(HitType hitType) { TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; From beed390031bf015910cbb2da91a2d8c313e24a03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:14:52 +0900 Subject: [PATCH 079/555] Add balance adjust to base implementation of `DrumSampleTriggerSource` --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 8 ++--- .../UI/DrumSamplePlayer.cs | 10 +++--- .../UI/DrumSampleTriggerSource.cs | 34 ++++++++++++++++++- .../UI/GameplaySampleTriggerSource.cs | 2 +- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 5b690be780..9ceea14802 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -10,13 +10,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { public partial class ArgonDrumSamplePlayer : DrumSamplePlayer { - protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) => - new ArgonDrumSampleTriggerSource(hitObjectContainer); + protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => + new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { - public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer) + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) + : base(hitObjectContainer, balance) { } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index b60ad209ff..410072994f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -25,14 +25,14 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChildren = new Drawable[] { - leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), - leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), - rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer), - rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer), + leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), }; } - protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer) + protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new DrumSampleTriggerSource(hitObjectContainer); public bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 939eba684b..50ff20f473 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,17 +2,35 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { public partial class DrumSampleTriggerSource : GameplaySampleTriggerSource { - public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer) + private const double stereo_separation = 0.2; + + public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance = SampleBalance.Centre) : base(hitObjectContainer) { + switch (balance) + { + case SampleBalance.Left: + AudioContainer.Balance.Value = -stereo_separation; + break; + + case SampleBalance.Centre: + AudioContainer.Balance.Value = 0; + break; + + case SampleBalance.Right: + AudioContainer.Balance.Value = stereo_separation; + break; + } } public virtual void Play(HitType hitType) @@ -39,5 +57,19 @@ namespace osu.Game.Rulesets.Taiko.UI } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); + + protected override void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples) + { + base.ApplySampleInfo(hitSound, samples); + + hitSound.Balance.Value = -0.05 + RNG.NextDouble(0.1); + } + } + + public enum SampleBalance + { + Left, + Centre, + Right } } diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index efbf823058..58410ea14b 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.UI } } - private SkinnableSound getNextSample() + protected SkinnableSound GetNextSample() { SkinnableSound hitSound = hitSounds[nextHitSoundIndex]; From 27af07b74be9a67b82bb8823d86f6044ef9ec044 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 16:17:45 +0900 Subject: [PATCH 080/555] Add basic implementation of argon osu!taiko hitsounds (volume / flourish / strong) --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 70 +++++++++++++++++-- osu.Game/Audio/HitSampleInfo.cs | 6 ++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 9ceea14802..b91e0f9901 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -1,10 +1,13 @@ // 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.Framework.Allocation; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { @@ -15,33 +18,90 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) : base(hitObjectContainer, balance) { + // TODO: pool flourish sample } public override void Play(HitType hitType) { - // let the magic begin... - TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; if (hitObject == null) return; - var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + var baseSample = new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL)); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(baseSample) != null) is LegacySkin) + { + base.Play(hitType); + return; + } + + // let the magic begin... if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { PlaySamples(new ISampleInfo[] { - hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH), + new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL), true), + // TODO: flourish should only play every time_between_flourishes. + new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_FLOURISH : string.Empty), true), baseSample }); } else { - PlaySamples(new ISampleInfo[] { baseSample }); + PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(baseSample) }); + } + } + + private class VolumeAwareHitSampleInfo : HitSampleInfo + { + public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; + public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; + + public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) + : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) + { + } + + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); + } + } + + private static string getBank(string originalBank, string sampleName, int volume) + { + // So basically we're overwriting mapper's bank intentions here. + // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. + + switch (sampleName) + { + case HIT_NORMAL: + case HIT_CLAP: + { + if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) + return BANK_DRUM; + + if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) + return BANK_NORMAL; + + return BANK_SOFT; + } + + default: + return originalBank; + } } } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 9573a9a4aa..24cb1730bf 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -24,6 +24,12 @@ namespace osu.Game.Audio public const string BANK_SOFT = @"soft"; public const string BANK_DRUM = @"drum"; + // new sample used exclusively by taiko for now. + public const string HIT_FLOURISH = "hitflourish"; + + // new bank used exclusively by taiko for now. + public const string BANK_STRONG = @"strong"; + /// /// All valid sample addition constants. /// From a9587fd1aa03f2916cf75d2987957b08238ed0de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 17:02:08 +0900 Subject: [PATCH 081/555] Move strong hit handling to `DrumSamplePlayer` and separte trigger sources --- .../Objects/Drawables/DrawableHit.cs | 6 +- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 3 +- .../UI/DrumSamplePlayer.cs | 106 +++++++++++++++--- .../UI/DrumSampleTriggerSource.cs | 1 + 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 5b79151225..44225ab289 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// The lenience for the second key press. /// This does not adjust by map difficulty in ScoreV2 yet. /// - private const double second_hit_window = 30; + public const double SECOND_HIT_WINDOW = 30; public StrongNestedHit() : this(null) @@ -223,12 +223,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { - if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window) + if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW) ApplyResult(r => r.Type = r.Judgement.MinResult); return; } - if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window) + if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW) ApplyResult(r => r.Type = r.Judgement.MaxResult); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index b91e0f9901..a692260f10 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -11,7 +11,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { - public partial class ArgonDrumSamplePlayer : DrumSamplePlayer + internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer { protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); @@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // let the magic begin... + // TODO: should we only play strong samples if the user correctly hits them? arguable. if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { PlaySamples(new ISampleInfo[] diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 410072994f..99449ecbda 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -7,28 +7,35 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.UI { - public partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler + internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler { - private DrumSampleTriggerSource leftRimSampleTriggerSource = null!; - private DrumSampleTriggerSource leftCentreSampleTriggerSource = null!; - private DrumSampleTriggerSource rightCentreSampleTriggerSource = null!; - private DrumSampleTriggerSource rightRimSampleTriggerSource = null!; + private DrumSampleTriggerSource leftCentreTrigger = null!; + private DrumSampleTriggerSource rightCentreTrigger = null!; + private DrumSampleTriggerSource leftRimTrigger = null!; + private DrumSampleTriggerSource rightRimTrigger = null!; + private DrumSampleTriggerSource strongCentreTrigger = null!; + private DrumSampleTriggerSource strongRimTrigger = null!; + + private double lastHitTime; + private TaikoAction? lastAction; [BackgroundDependencyLoader] private void load(DrawableRuleset drawableRuleset) { var hitObjectContainer = drawableRuleset.Playfield.HitObjectContainer; - InternalChildren = new Drawable[] { - leftRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), - leftCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), - rightCentreSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), - rightRimSampleTriggerSource = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + leftRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + strongCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre), + strongRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre) }; } @@ -37,28 +44,93 @@ namespace osu.Game.Rulesets.Taiko.UI public bool OnPressed(KeyBindingPressEvent e) { + HitType hitType; + + DrumSampleTriggerSource triggerSource; + + bool strong = checkStrongValidity(e.Action, lastAction, Time.Current - lastHitTime); + switch (e.Action) { - case TaikoAction.LeftRim: - leftRimSampleTriggerSource.Play(HitType.Rim); - break; - case TaikoAction.LeftCentre: - leftCentreSampleTriggerSource.Play(HitType.Centre); + hitType = HitType.Centre; + triggerSource = strong ? strongCentreTrigger : leftCentreTrigger; break; case TaikoAction.RightCentre: - rightCentreSampleTriggerSource.Play(HitType.Centre); + hitType = HitType.Centre; + triggerSource = strong ? strongCentreTrigger : rightCentreTrigger; + break; + + case TaikoAction.LeftRim: + hitType = HitType.Rim; + triggerSource = strong ? strongRimTrigger : leftRimTrigger; break; case TaikoAction.RightRim: - rightRimSampleTriggerSource.Play(HitType.Rim); + hitType = HitType.Rim; + triggerSource = strong ? strongRimTrigger : rightRimTrigger; break; + + default: + return false; } + if (strong && hitType == HitType.Centre) + flushCenterTriggerSources(); + + if (strong && hitType == HitType.Rim) + flushRimTriggerSources(); + + triggerSource.Play(hitType); + + lastHitTime = Time.Current; + lastAction = e.Action; + return false; } + private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions) + { + if (lastAction == null) + return false; + + if (timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW) + return false; + + switch (newAction) + { + case TaikoAction.LeftCentre: + return lastAction == TaikoAction.RightCentre; + + case TaikoAction.RightCentre: + return lastAction == TaikoAction.LeftCentre; + + case TaikoAction.LeftRim: + return lastAction == TaikoAction.RightRim; + + case TaikoAction.RightRim: + return lastAction == TaikoAction.LeftRim; + + default: + return false; + } + } + + private void flushCenterTriggerSources() + { + leftCentreTrigger.StopAllPlayback(); + rightCentreTrigger.StopAllPlayback(); + strongCentreTrigger.StopAllPlayback(); + } + + private void flushRimTriggerSources() + { + leftRimTrigger.StopAllPlayback(); + rightRimTrigger.StopAllPlayback(); + strongRimTrigger.StopAllPlayback(); + } + public void OnReleased(KeyBindingReleaseEvent e) { } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 50ff20f473..a205d4fee1 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + // TODO: should we only play strong samples if the user correctly hits them? arguable. if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) { PlaySamples(new ISampleInfo[] From 010262c764cbcd58280f2b209a541476068f078f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 17:50:32 +0900 Subject: [PATCH 082/555] Change strong hit sample handling to be user input based, not hit object based --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 7 +++---- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 +- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index a692260f10..5996ba2d05 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // TODO: pool flourish sample } - public override void Play(HitType hitType) + public override void Play(HitType hitType, bool strong) { TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; @@ -39,14 +39,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // If the sample is provided by a legacy skin, we should not try and do anything special. if (skinSource.FindProvider(s => s.GetSample(baseSample) != null) is LegacySkin) { - base.Play(hitType); + base.Play(hitType, strong); return; } // let the magic begin... - // TODO: should we only play strong samples if the user correctly hits them? arguable. - if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) + if (strong) { PlaySamples(new ISampleInfo[] { diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 99449ecbda..dba61a9b41 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (strong && hitType == HitType.Rim) flushRimTriggerSources(); - triggerSource.Play(hitType); + triggerSource.Play(hitType, strong); lastHitTime = Time.Current; lastAction = e.Action; diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index a205d4fee1..c87f95430f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - public virtual void Play(HitType hitType) + public virtual void Play(HitType hitType, bool strong) { TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); // TODO: should we only play strong samples if the user correctly hits them? arguable. - if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject) + if (strong) { PlaySamples(new ISampleInfo[] { From c72ebcfd539f8cca5b95dae78512d0f24ac7e4aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 18:09:50 +0900 Subject: [PATCH 083/555] Fix skin fallback not working as expected --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 5996ba2d05..67d417c599 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -34,10 +34,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon if (hitObject == null) return; - var baseSample = new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL)); + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); // If the sample is provided by a legacy skin, we should not try and do anything special. - if (skinSource.FindProvider(s => s.GetSample(baseSample) != null) is LegacySkin) + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) { base.Play(hitType, strong); return; @@ -49,15 +49,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { PlaySamples(new ISampleInfo[] { - new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL), true), + new VolumeAwareHitSampleInfo(originalSample, true), // TODO: flourish should only play every time_between_flourishes. new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_FLOURISH : string.Empty), true), - baseSample + new VolumeAwareHitSampleInfo(originalSample) }); } else { - PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(baseSample) }); + PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(new VolumeAwareHitSampleInfo(originalSample)) }); } } From 16f1a7694d9397a9acac2acfd0c39f38ef438547 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Jun 2023 18:43:37 +0900 Subject: [PATCH 084/555] Add time-based flourish support --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 67d417c599..72e5ac5b3d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; @@ -18,12 +19,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { + private readonly HitObjectContainer hitObjectContainer; + [Resolved] private ISkinSource skinSource { get; set; } = null!; + /// + /// The minimum time to leave between flourishes that are added to strong rim hits. + /// + private const double time_between_flourishes = 2000; + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) : base(hitObjectContainer, balance) { + this.hitObjectContainer = hitObjectContainer; // TODO: pool flourish sample } @@ -44,21 +53,41 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon } // let the magic begin... + var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; - if (strong) + if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) + samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + + PlaySamples(samplesToPlay.ToArray()); + } + + private bool canPlayFlourish(TaikoHitObject hitObject) + { + double? lastFlourish = null; + + // TODO: check on nested strong hits. we're not accounting for them here yet. + + var hitObjects = hitObjectContainer.AliveObjects + .Reverse() + .Select(d => d.HitObject) + .OfType() + .Where(h => h.IsStrong && h.Type == HitType.Rim); + + // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). + // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to + // end of groups/combos of strong rim hits instead of the start. + foreach (var h in hitObjects) { - PlaySamples(new ISampleInfo[] - { - new VolumeAwareHitSampleInfo(originalSample, true), - // TODO: flourish should only play every time_between_flourishes. - new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_FLOURISH : string.Empty), true), - new VolumeAwareHitSampleInfo(originalSample) - }); - } - else - { - PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(new VolumeAwareHitSampleInfo(originalSample)) }); + bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; + + if (canFlourish) + lastFlourish = h.StartTime; + + if (h == hitObject) + return canFlourish; } + + return false; } private class VolumeAwareHitSampleInfo : HitSampleInfo From f08690883118dfaa5bf78af204178d17a007f771 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jul 2023 13:49:14 +0900 Subject: [PATCH 085/555] Don't attempt to play drum samples when rewinding --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index dba61a9b41..4f727e8c90 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Taiko.UI public bool OnPressed(KeyBindingPressEvent e) { + if (Time.Elapsed < 0) + return false; + HitType hitType; DrumSampleTriggerSource triggerSource; From 759cd5aec79686df8553efc5582524b27d0044c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jul 2023 14:05:40 +0900 Subject: [PATCH 086/555] Warm up pool with argon-specific drum samples --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 14 ++++++++++---- osu.Game/Skinning/IPooledSampleProvider.cs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 72e5ac5b3d..91393d99fe 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -14,6 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer { + [BackgroundDependencyLoader] + private void load(IPooledSampleProvider sampleProvider) + { + // Warm up pools for non-standard samples. + sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); + sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); + sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + } + protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); @@ -33,7 +42,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon : base(hitObjectContainer, balance) { this.hitObjectContainer = hitObjectContainer; - // TODO: pool flourish sample } public override void Play(HitType hitType, bool strong) @@ -65,8 +73,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { double? lastFlourish = null; - // TODO: check on nested strong hits. we're not accounting for them here yet. - var hitObjects = hitObjectContainer.AliveObjects .Reverse() .Select(d => d.HitObject) @@ -90,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return false; } - private class VolumeAwareHitSampleInfo : HitSampleInfo + public class VolumeAwareHitSampleInfo : HitSampleInfo { public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs index 3ea299f5e2..0e57050c4d 100644 --- a/osu.Game/Skinning/IPooledSampleProvider.cs +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -8,7 +8,7 @@ namespace osu.Game.Skinning /// /// Provides pooled samples to be used by s. /// - internal interface IPooledSampleProvider + public interface IPooledSampleProvider { /// /// Retrieves a from a pool. From 561fff801a5c33d28f67b08ede925f17fdc5b8d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jul 2023 13:29:44 +0900 Subject: [PATCH 087/555] Consume nested object states in `HitObjectLifetimeEntry` --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 58410ea14b..18d412ab44 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.UI return getAllNested(mostValidObject.HitObject).OrderBy(h => h.GetEndTime()).SkipWhile(h => h.GetEndTime() <= getReferenceTime()).FirstOrDefault() ?? mostValidObject.HitObject; } - private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true; + private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.AllJudged; private bool isCloseEnoughToCurrentTime(HitObject h) => getReferenceTime() >= h.StartTime - h.HitWindows.WindowFor(HitResult.Miss) * 2; private double getReferenceTime() => gameplayClock?.CurrentTime ?? Clock.CurrentTime; From 8413247773ba495307a15085ec2302fce690e901 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 13:10:02 +0900 Subject: [PATCH 088/555] Fix failing test --- .../TestSceneDrumSampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 4133b96d42..06e88643de 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -372,7 +372,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank) { - AddStep($"hit {hitType}", () => triggerSource.Play(hitType)); + AddStep($"hit {hitType}", () => triggerSource.Play(hitType, false)); 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)); From 4364736ccd6f8a0d06e654dd9fee061910997643 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:02:03 +0900 Subject: [PATCH 089/555] 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 168b6c70a98ed59b56d52572bcfe29abcf1ab6cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 15:12:40 +0900 Subject: [PATCH 090/555] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b4d8dd513f..83d4bcb336 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 7355a6a66bf695a0addd9cd2a1f5a310edfc284f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:20:25 +0900 Subject: [PATCH 091/555] 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 51b0d18c04e458b7785db34dfe4613e682bf7396 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 17:41:51 +0900 Subject: [PATCH 092/555] Fix weird test assertion output --- .../Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 6701871e8d..59c9c4587a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.Gameplay } private void checkValidObjectIndex(int index) => - AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index])); + AddAssert($"check object at index {index} is correct", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index])); private DrawableHitObject? getNextAliveObject() => Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(); From 289f916cd7e37bcb6164a5412ea86f7db25cae94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:36:41 +0900 Subject: [PATCH 093/555] Remove outdated TODO --- osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index c87f95430f..1de16c2294 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Taiko.UI var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); - // TODO: should we only play strong samples if the user correctly hits them? arguable. if (strong) { PlaySamples(new ISampleInfo[] From 8f6b06fe4004bfc3be183080fee05d0e802b05eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:38:19 +0900 Subject: [PATCH 094/555] Update test assumptions --- .../TestSceneDrumSampleTriggerSource.cs | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 06e88643de..ced2e4b98c 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); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, 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); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, 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)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(120); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); seekTo(480); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(700); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{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); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, 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); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{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); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -356,23 +356,23 @@ 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); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(600); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); seekTo(1200); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); - checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); } - private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank) + private void checkSamples(HitType hitType, bool strong, string expectedSamplesCsv, string expectedBank) { - AddStep($"hit {hitType}", () => triggerSource.Play(hitType, false)); + AddStep($"hit {hitType}", () => triggerSource.Play(hitType, strong)); 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)); From 8f61f5e4c6c60d0c8933756929f16efd6728f279 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 18:41:32 +0900 Subject: [PATCH 095/555] Cache `Playfield` for the sake of tests I'm open to an alternative. Name it. --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 4 ++-- osu.Game/Rulesets/UI/Playfield.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 4f727e8c90..2797492b0a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -25,9 +25,9 @@ namespace osu.Game.Rulesets.Taiko.UI private TaikoAction? lastAction; [BackgroundDependencyLoader] - private void load(DrawableRuleset drawableRuleset) + private void load(Playfield playfield) { - var hitObjectContainer = drawableRuleset.Playfield.HitObjectContainer; + var hitObjectContainer = playfield.HitObjectContainer; InternalChildren = new Drawable[] { leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 6016a53918..3f263aba63 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.UI { [Cached(typeof(IPooledHitObjectProvider))] [Cached(typeof(IPooledSampleProvider))] + [Cached] public abstract partial class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider { /// From 00c68cad532d5898f5d6135d6df202db00caf9f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Jul 2023 19:47:44 +0900 Subject: [PATCH 096/555] 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 097/555] 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 098/555] 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 099/555] 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 100/555] 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 101/555] 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[] From 170bc5bfcec23252a836c31829aa971af778ce84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 12:25:15 +0900 Subject: [PATCH 102/555] Add support for skinnable "retry" sound --- osu.Game/Screens/Play/Player.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b979fc2740..2cb7748a15 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -11,8 +11,6 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -114,7 +112,7 @@ namespace osu.Game.Screens.Play private Ruleset ruleset; - private Sample sampleRestart; + private SkinnableSound sampleRestart; public BreakOverlay BreakOverlay; @@ -195,7 +193,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken) + private void load(OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken) { var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray(); @@ -213,8 +211,6 @@ namespace osu.Game.Screens.Play if (playableBeatmap == null) return; - sampleRestart = audio.Samples.Get(@"Gameplay/restart"); - mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) @@ -295,15 +291,19 @@ namespace osu.Game.Screens.Play if (Configuration.AllowRestart) { - rulesetSkinProvider.Add(new HotkeyRetryOverlay + rulesetSkinProvider.AddRange(new Drawable[] { - Action = () => + new HotkeyRetryOverlay { - if (!this.IsCurrentScreen()) return; + Action = () => + { + if (!this.IsCurrentScreen()) return; - fadeOut(true); - Restart(true); + fadeOut(true); + Restart(true); + }, }, + sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }); } From b679ab88a161cca1331c5bf939927c498af44df0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 12:29:03 +0900 Subject: [PATCH 103/555] Avoid attempting to process missing statistics on scores without linked beatmaps --- osu.Game/BackgroundBeatmapProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9fe3a41b03..9c140bdda9 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -163,8 +163,12 @@ namespace osu.Game { foreach (var score in r.All()) { - if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0) + if (score.BeatmapInfo != null + && score.Statistics.Sum(kvp => kvp.Value) > 0 + && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0) + { scoreIds.Add(score.ID); + } } }); From a98a36872e286da460737d92e0658713ba690292 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 13:37:42 +0900 Subject: [PATCH 104/555] Bring realm library up-to-date --- .../Beatmaps/WorkingBeatmapManagerTest.cs | 6 ++--- .../BackgroundBeatmapProcessorTests.cs | 14 +++++------ .../Database/BeatmapImporterUpdateTests.cs | 14 +++++------ osu.Game.Tests/Database/GeneralUsageTests.cs | 2 +- osu.Game.Tests/Database/RealmLiveTests.cs | 2 +- .../RealmSubscriptionRegistrationTests.cs | 7 +++--- osu.Game.Tests/Database/RulesetStoreTests.cs | 10 ++++---- .../TestScenePlayerLocalScoreImport.cs | 2 +- .../UserInterface/TestSceneModPresetColumn.cs | 6 ++--- .../TestSceneModSelectOverlay.cs | 4 ++-- osu.Game/BackgroundBeatmapProcessor.cs | 6 ++--- osu.Game/Beatmaps/BeatmapImporter.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 8 +++---- osu.Game/Beatmaps/BeatmapUpdater.cs | 2 +- osu.Game/Collections/CollectionDropdown.cs | 2 +- .../Collections/DrawableCollectionList.cs | 2 +- .../Collections/DrawableCollectionListItem.cs | 2 +- osu.Game/Database/EmptyRealmSet.cs | 4 ++-- osu.Game/Database/ModelManager.cs | 24 ++++++++++--------- osu.Game/Database/RealmAccess.cs | 14 +++++------ osu.Game/Database/RealmLive.cs | 6 ++--- osu.Game/Database/RealmObjectExtensions.cs | 8 +++---- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/Online/BeatmapDownloadTracker.cs | 2 +- .../OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game/Online/ScoreDownloadTracker.cs | 2 +- .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 2 +- osu.Game/Overlays/Mods/AddPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/ModPresetColumn.cs | 2 +- osu.Game/Overlays/Music/PlaylistOverlay.cs | 2 +- .../Overlays/Settings/Sections/SkinSection.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 +++---- .../Screens/Select/Carousel/TopLocalRank.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- osu.Game/Skinning/RealmBackedResourceStore.cs | 2 +- osu.Game/Skinning/SkinImporter.cs | 6 ++--- osu.Game/osu.Game.csproj | 2 +- 39 files changed, 97 insertions(+), 96 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs index 89b8c8927d..237fe758b5 100644 --- a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs +++ b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestCachedRetrievalWithFiles() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0)); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0)); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSavePreservesCollections() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); var working = beatmaps.GetWorkingBeatmap(beatmap); diff --git a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs b/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs index ddb60606ec..c876316be4 100644 --- a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Database { Realm.Write(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; foreach (var b in beatmapSetInfo.Beatmaps) b.StarRating = -1; }); @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Database { Realm.Write(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; foreach (var b in beatmapSetInfo.Beatmaps) b.StarRating = -1; }); @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1); }); }); @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 83cb54df3f..437a8c75e2 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -323,7 +323,7 @@ namespace osu.Game.Tests.Database var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename); scoreTargetBeatmapHash = beatmapInfo.Hash; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); realm.Run(r => r.Refresh()); @@ -372,7 +372,7 @@ namespace osu.Game.Tests.Database scoreTargetBeatmapHash = beatmapInfo.Hash; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); // locally modify beatmap @@ -435,7 +435,7 @@ namespace osu.Game.Tests.Database { var beatmapInfo = s.Beatmaps.Last(); scoreTargetFilename = beatmapInfo.File?.Filename; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); realm.Run(r => r.Refresh()); @@ -528,7 +528,7 @@ namespace osu.Game.Tests.Database importBeforeUpdate.PerformWrite(s => { - var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection")); + var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection")); beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1); for (int i = 0; i < beatmapsToAddToCollection; i++) @@ -543,7 +543,7 @@ namespace osu.Game.Tests.Database importAfterUpdate.PerformRead(updated => { - updated.Realm.Refresh(); + updated.Realm!.Refresh(); string[] hashes = updated.Realm.All().Single().BeatmapMD5Hashes.ToArray(); @@ -593,7 +593,7 @@ namespace osu.Game.Tests.Database importBeforeUpdate.PerformWrite(s => { - var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection")); + var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection")); originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash; beatmapCollection.BeatmapMD5Hashes.Add(originalHash); @@ -607,7 +607,7 @@ namespace osu.Game.Tests.Database importAfterUpdate.PerformRead(updated => { - updated.Realm.Refresh(); + updated.Realm!.Refresh(); string[] hashes = updated.Realm.All().Single().BeatmapMD5Hashes.ToArray(); string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash; diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index fd0b391d0d..b8073a65bc 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Database realm.RegisterCustomSubscription(r => { - var subscription = r.All().QueryAsyncWithNotifications((_, _, _) => + var subscription = r.All().QueryAsyncWithNotifications((_, _) => { realm.Run(_ => { diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index d853e75db0..cea30acf3f 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Database return null; }); - void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) + void gotChange(IRealmCollection sender, ChangeSet? changes) { changesTriggered++; } diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 4ee302bbd0..45842a952a 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -54,7 +53,7 @@ namespace osu.Game.Tests.Database registration.Dispose(); }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + void onChanged(IRealmCollection sender, ChangeSet? changes) { lastChanges = changes; @@ -92,7 +91,7 @@ namespace osu.Game.Tests.Database registration.Dispose(); }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes; + void onChanged(IRealmCollection sender, ChangeSet? changes) => lastChanges = changes; } [Test] @@ -185,7 +184,7 @@ namespace osu.Game.Tests.Database } }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + void onChanged(IRealmCollection sender, ChangeSet? changes) { if (changes == null) resolvedItems = sender; diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index a5662fa121..8b4c6e2411 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -76,12 +76,12 @@ namespace osu.Game.Tests.Database Available = true, })); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore var _ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); }); } @@ -101,18 +101,18 @@ namespace osu.Game.Tests.Database Available = true, })); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore var _ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); // Simulate the ruleset getting updated LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; var __ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 80c4e4bce9..b0b9d48cbe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First())); AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); - AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First())); + AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First())); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 2d54a4e566..bf6d8e524f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface var testPresets = createTestPresets(); foreach (var preset in testPresets) - preset.Ruleset = realm.Find(preset.Ruleset.ShortName); + preset.Ruleset = realm.Find(preset.Ruleset.ShortName)!; realm.Add(testPresets); }); @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.UserInterface new ManiaModNightcore(), new ManiaModHardRock() }, - Ruleset = r.Find("mania") + Ruleset = r.Find("mania")! }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface new OsuModHidden(), new OsuModHardRock() }, - Ruleset = r.Find("osu") + Ruleset = r.Find("osu")! }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index dcb1f730a2..4cb6899ebc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Name = "AR0", Description = "Too... many... circles...", - Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Ruleset = r.Find(OsuRuleset.SHORT_NAME)!, Mods = new[] { new OsuModDifficultyAdjust @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Name = "Half Time 0.5x", Description = "Very slow", - Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Ruleset = r.Find(OsuRuleset.SHORT_NAME)!, Mods = new[] { new OsuModHalfTime diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9fe3a41b03..34c1786682 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -102,7 +102,7 @@ namespace osu.Game } } - r.Find(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion; + r.Find(ruleset.ShortName)!.LastAppliedDifficultyVersion = currentVersion; }); Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}"); @@ -184,7 +184,7 @@ namespace osu.Game // ReSharper disable once MethodHasAsyncOverload realmAccess.Write(r => { - r.Find(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); + r.Find(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); }); Logger.Log($"Populated maximum statistics for score {id}"); @@ -237,7 +237,7 @@ namespace osu.Game // ReSharper disable once MethodHasAsyncOverload realmAccess.Write(r => { - ScoreInfo s = r.Find(id); + ScoreInfo s = r.Find(id)!; s.TotalScore = newTotalScore; s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index fd766490fc..dba3987524 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database); - original = realm.Find(original.ID); + original = realm!.Find(original.ID)!; // Generally the import process will do this for us if the OnlineIDs match, // but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated). diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 73811b2e62..cf5ed25404 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -208,7 +208,7 @@ namespace osu.Game.Beatmaps using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; beatmapInfo.Hidden = true; transaction.Commit(); @@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; beatmapInfo.Hidden = false; transaction.Commit(); @@ -330,7 +330,7 @@ namespace osu.Game.Beatmaps Realm.Write(r => { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; Debug.Assert(beatmapInfo.BeatmapSet != null); Debug.Assert(beatmapInfo.File != null); @@ -460,7 +460,7 @@ namespace osu.Game.Beatmaps Realm.Write(r => { - var liveBeatmapSet = r.Find(setInfo.ID); + var liveBeatmapSet = r.Find(setInfo.ID)!; setInfo.CopyChangesToRealm(liveBeatmapSet); diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index 046adb8327..56bfdc5001 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps /// /// The managed beatmap set to update. A transaction will be opened to apply changes. /// The preferred scope to use for metadata lookup. - public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r => + public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ => { // Before we use below, we want to invalidate. workingBeatmapCache.Invalidate(beatmapSet); diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index e95565a5c8..e435992381 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -59,7 +59,7 @@ namespace osu.Game.Collections Current.BindValueChanged(selectionChanged); } - private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { var selectedItem = SelectedItem?.Value?.Collection; diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 0fdf196c4a..6fe38a3229 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -41,7 +41,7 @@ namespace osu.Game.Collections realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); } - private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { Items.Clear(); Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm))); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 0ab0ff520d..4131148f3f 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -197,7 +197,7 @@ namespace osu.Game.Collections return true; } - private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c)); + private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } } } diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs index 7db946d79f..02dfa50fe5 100644 --- a/osu.Game/Database/EmptyRealmSet.cs +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -19,8 +19,8 @@ namespace osu.Game.Database IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator(); public int Count => emptySet.Count; public T this[int index] => emptySet[index]; - public int IndexOf(object item) => emptySet.IndexOf((T)item); - public bool Contains(object item) => emptySet.Contains((T)item); + public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item); + public bool Contains(object? item) => item != null && emptySet.Contains((T)item); public event NotifyCollectionChangedEventHandler? CollectionChanged { diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 7d1dc5239a..47feb8a8f9 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -34,13 +34,13 @@ namespace osu.Game.Database } public void DeleteFile(TModel item, RealmNamedFileUsage file) => - performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); + performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm!)); public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) => - performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm)); + performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm!)); public void AddFile(TModel item, Stream contents, string filename) => - performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm)); + performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm!)); private void performFileOperation(TModel item, Action operation) { @@ -178,13 +178,14 @@ namespace osu.Game.Database // (ie. if an async import finished very recently). return Realm.Write(realm => { - if (!item.IsManaged) - item = realm.Find(item.ID); + TModel? processableItem = item; + if (!processableItem.IsManaged) + processableItem = realm.Find(item.ID); - if (item?.DeletePending != false) + if (processableItem?.DeletePending != false) return false; - item.DeletePending = true; + processableItem.DeletePending = true; return true; }); } @@ -195,13 +196,14 @@ namespace osu.Game.Database // (ie. if an async import finished very recently). Realm.Write(realm => { - if (!item.IsManaged) - item = realm.Find(item.ID); + TModel? processableItem = item; + if (!processableItem.IsManaged) + processableItem = realm.Find(item.ID); - if (item?.DeletePending != true) + if (processableItem?.DeletePending != true) return; - item.DeletePending = false; + processableItem.DeletePending = false; }); } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 2bc932f307..a6cef82c5f 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -535,7 +535,7 @@ namespace osu.Game.Database lock (notificationsResetMap) { // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. - notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null)); + notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null)); } return RegisterCustomSubscription(action); @@ -755,10 +755,10 @@ namespace osu.Game.Database for (int i = 0; i < itemCount; i++) { - dynamic? oldItem = oldItems.ElementAt(i); - dynamic? newItem = newItems.ElementAt(i); + dynamic oldItem = oldItems.ElementAt(i); + dynamic newItem = newItems.ElementAt(i); - long? nullableOnlineID = oldItem?.OnlineID; + long? nullableOnlineID = oldItem.OnlineID; newItem.OnlineID = (int)(nullableOnlineID ?? -1); } } @@ -795,7 +795,7 @@ namespace osu.Game.Database for (int i = 0; i < metadataCount; i++) { - dynamic? oldItem = oldMetadata.ElementAt(i); + dynamic oldItem = oldMetadata.ElementAt(i); var newItem = newMetadata.ElementAt(i); string username = oldItem.Author; @@ -818,7 +818,7 @@ namespace osu.Game.Database for (int i = 0; i < newSettings.Count; i++) { - dynamic? oldItem = oldSettings.ElementAt(i); + dynamic oldItem = oldSettings.ElementAt(i); var newItem = newSettings.ElementAt(i); long rulesetId = oldItem.RulesetID; @@ -843,7 +843,7 @@ namespace osu.Game.Database for (int i = 0; i < newKeyBindings.Count; i++) { - dynamic? oldItem = oldKeyBindings.ElementAt(i); + dynamic oldItem = oldKeyBindings.ElementAt(i); var newItem = newKeyBindings.ElementAt(i); if (oldItem.RulesetID == null) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 9c871a3929..d96ccd9dcd 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -104,7 +104,7 @@ namespace osu.Game.Database PerformRead(t => { - using (var transaction = t.Realm.BeginWrite()) + using (var transaction = t.Realm!.BeginWrite()) { perform(t); transaction.Commit(); @@ -133,7 +133,7 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - if (dataIsFromUpdateThread && !data.Realm.IsClosed) + if (dataIsFromUpdateThread && !data.Realm!.IsClosed) { RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++; return; @@ -154,7 +154,7 @@ namespace osu.Game.Database // To ensure that behaviour matches what we'd expect (the object *is* available), force // a refresh to bring in any off-thread changes immediately. realm.Refresh(); - found = realm.Find(ID); + found = realm.Find(ID)!; } return found; diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 5a6c2e3232..77d199cf9c 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -43,7 +43,7 @@ namespace osu.Game.Database .ForMember(s => s.BeatmapSet, cc => cc.Ignore()) .AfterMap((s, d) => { - d.Ruleset = d.Realm.Find(s.Ruleset.ShortName); + d.Ruleset = d.Realm!.Find(s.Ruleset.ShortName)!; copyChangesToRealm(s.Difficulty, d.Difficulty); copyChangesToRealm(s.Metadata, d.Metadata); }); @@ -57,7 +57,7 @@ namespace osu.Game.Database // Importantly, search all of realm for the beatmap (not just the set's beatmaps). // It may have gotten detached, and if that's the case let's use this opportunity to fix // things up. - var existingBeatmap = d.Realm.Find(beatmap.ID); + var existingBeatmap = d.Realm!.Find(beatmap.ID); if (existingBeatmap != null) { @@ -77,7 +77,7 @@ namespace osu.Game.Database { ID = beatmap.ID, BeatmapSet = d, - Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName) + Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName)! }; d.Beatmaps.Add(newBeatmap); @@ -287,7 +287,7 @@ namespace osu.Game.Database /// /// /// - public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) + public static IDisposable QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { if (!RealmAccess.CurrentThreadSubscriptionsAllowed) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index fab0be6cf0..be025e3aa2 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _, _) => + realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 144c4445a3..3db602c353 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _, _) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 331a471ad5..ceb8e53778 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -107,7 +107,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes, _) => + realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes) => { if (changes == null) return; diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 4ddcb40368..de42292372 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -48,7 +48,7 @@ namespace osu.Game.Online realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) - && !s.DeletePending), (items, _, _) => + && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index 75bc8fd3a8..385695f669 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup beatmapSubscription?.Dispose(); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => Schedule(() => + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) => Schedule(() => { currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count); diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index d9e350e560..ef855f6166 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Mods Name = nameTextBox.Current.Value, Description = descriptionTextBox.Current.Value, Mods = selectedMods.Value.ToArray(), - Ruleset = r.Find(ruleset.Value.ShortName) + Ruleset = r.Find(ruleset.Value.ShortName)! })); this.HidePopover(); diff --git a/osu.Game/Overlays/Mods/ModPresetColumn.cs b/osu.Game/Overlays/Mods/ModPresetColumn.cs index bf5e576277..3b12eec195 100644 --- a/osu.Game/Overlays/Mods/ModPresetColumn.cs +++ b/osu.Game/Overlays/Mods/ModPresetColumn.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Mods private Task? latestLoadTask; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true; - private void asyncLoadPanels(IRealmCollection presets, ChangeSet changes, Exception error) + private void asyncLoadPanels(IRealmCollection presets, ChangeSet? changes) { cancellationTokenSource?.Cancel(); diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 43b9024303..7784643163 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -109,7 +109,7 @@ namespace osu.Game.Overlays.Music beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet changes) { if (changes == null) { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5382eac675..e997e70157 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections }); } - private void skinsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void skinsChanged(IRealmCollection sender, ChangeSet changes) { // This can only mean that realm is recycling, else we would see the protected skins. // Because we are using `Live<>` in this class, we don't need to worry about this scenario too much. diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 5ada2a410d..264a23b6d3 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -66,10 +66,10 @@ namespace osu.Game.Scoring { // Ensure the beatmap is not detached. if (!model.BeatmapInfo.IsManaged) - model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID); + model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID)!; if (!model.Ruleset.IsManaged) - model.Ruleset = realm.Find(model.Ruleset.ShortName); + model.Ruleset = realm.Find(model.Ruleset.ShortName)!; // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). // Under no circumstance do we want these to be written to realm as null. diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3d87a57295..9af9a0ce72 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -223,7 +223,7 @@ namespace osu.Game.Screens.Select subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } - private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -236,7 +236,7 @@ namespace osu.Game.Screens.Select removeBeatmapSet(sender[i].ID); } - private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -255,7 +255,7 @@ namespace osu.Game.Screens.Select foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - UpdateBeatmapSet(realm.Realm.Find(id).Detach()); + UpdateBeatmapSet(realm.Realm.Find(id)!.Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) @@ -315,7 +315,7 @@ namespace osu.Game.Screens.Select } } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { // we only care about actual changes in hidden status. if (changes == null) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 7c632b63db..c17de77619 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel localScoresChanged); }, true); - void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception _) + void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { // This subscription may fire from changes to linked beatmaps, which we don't care about. // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 4c41ed3622..58c14b15b9 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -193,7 +193,7 @@ namespace osu.Game.Screens.Select.Leaderboards + $" AND {nameof(ScoreInfo.DeletePending)} == false" , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception exception) + void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { if (cancellationToken.IsCancellationRequested) return; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 2b56767bd0..48b5c210b8 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Spectate })); } - private void beatmapsChanged(IRealmCollection items, ChangeSet changes, Exception ___) + private void beatmapsChanged(IRealmCollection items, ChangeSet changes) { if (changes?.InsertedIndices == null) return; diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index cc887a7a61..cce099a268 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -38,7 +38,7 @@ namespace osu.Game.Skinning realmSubscription?.Dispose(); } - private void skinChanged(IRealmCollection sender, ChangeSet changes, Exception error) => invalidateCache(); + private void skinChanged(IRealmCollection sender, ChangeSet? changes) => invalidateCache(); protected override IEnumerable GetFilenames(string name) { diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 43760c4a19..f2103a45c4 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -198,7 +198,7 @@ namespace osu.Game.Skinning using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(skinInfoJson))) { - modelManager.AddFile(s, streamContent, skin_info_file, s.Realm); + modelManager.AddFile(s, streamContent, skin_info_file, s.Realm!); } // Then serialise each of the drawable component groups into respective files. @@ -213,9 +213,9 @@ namespace osu.Game.Skinning var oldFile = s.GetFile(filename); if (oldFile != null) - modelManager.ReplaceFile(oldFile, streamContent, s.Realm); + modelManager.ReplaceFile(oldFile, streamContent, s.Realm!); else - modelManager.AddFile(s, streamContent, filename, s.Realm); + modelManager.AddFile(s, streamContent, filename, s.Realm!); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b4d8dd513f..41712f428a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 5af4aa874158157ce99aa7c9a1383c4b020409c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:05:47 +0900 Subject: [PATCH 105/555] Avoid strong hits cutting off other strong hits --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 2797492b0a..7144fd8ca5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -79,11 +79,19 @@ namespace osu.Game.Rulesets.Taiko.UI return false; } - if (strong && hitType == HitType.Centre) - flushCenterTriggerSources(); + if (strong) + { + switch (hitType) + { + case HitType.Centre: + flushCenterTriggerSources(); + break; - if (strong && hitType == HitType.Rim) - flushRimTriggerSources(); + case HitType.Rim: + flushRimTriggerSources(); + break; + } + } triggerSource.Play(hitType, strong); @@ -124,14 +132,12 @@ namespace osu.Game.Rulesets.Taiko.UI { leftCentreTrigger.StopAllPlayback(); rightCentreTrigger.StopAllPlayback(); - strongCentreTrigger.StopAllPlayback(); } private void flushRimTriggerSources() { leftRimTrigger.StopAllPlayback(); rightRimTrigger.StopAllPlayback(); - strongRimTrigger.StopAllPlayback(); } public void OnReleased(KeyBindingReleaseEvent e) From af3f9086e51a5f54f914a813f70685640236c4c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:02:20 +0900 Subject: [PATCH 106/555] Expose rewinding state of `IGameplayClock`s The implementation of this requires a bit of a special case for 0, so makes sense to implement in a central place. --- .../Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs | 1 + osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 +++++ osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 5 +++++ osu.Game/Screens/Play/GameplayClockContainer.cs | 5 ++--- osu.Game/Screens/Play/IGameplayClock.cs | 8 ++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index bcb5291108..db06329d74 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -125,6 +125,7 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable NonGameplayAdjustments => throw new NotImplementedException(); public IBindable IsPaused => throw new NotImplementedException(); + public bool IsRewinding => false; } } } diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 080b0ce7ec..399e2fde7f 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -64,6 +64,8 @@ namespace osu.Game.Beatmaps [Resolved] private IBindable beatmap { get; set; } = null!; + public bool IsRewinding { get; private set; } + public bool IsCoupled { get => decoupledClock.IsCoupled; @@ -133,6 +135,9 @@ namespace osu.Game.Beatmaps } else finalClockSource.ProcessFrame(); + + if (Clock.ElapsedFrameTime != 0) + IsRewinding = Clock.ElapsedFrameTime < 0; } public double TotalAppliedOffset diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4bb145973d..90cffab714 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -171,6 +171,9 @@ namespace osu.Game.Rulesets.UI // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed framedClock.ProcessFrame(); + + if (framedClock.ElapsedFrameTime != 0) + IsRewinding = framedClock.ElapsedFrameTime < 0; } /// @@ -247,6 +250,8 @@ namespace osu.Game.Rulesets.UI public IBindable IsPaused { get; } = new BindableBool(); + public bool IsRewinding { get; private set; } + public double CurrentTime => framedClock.CurrentTime; public double Rate => framedClock.Rate; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c42f607908..22e6884526 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -19,11 +19,10 @@ namespace osu.Game.Screens.Play [Cached(typeof(IGameplayClock))] public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { - /// - /// Whether gameplay is paused. - /// public IBindable IsPaused => isPaused; + public bool IsRewinding => GameplayClock.IsRewinding; + /// /// The source clock. Should generally not be used for any timekeeping purposes. /// diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index 83ba5f3474..ad28e343ff 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -23,6 +23,14 @@ namespace osu.Game.Screens.Play /// IAdjustableAudioComponent AdjustmentsFromMods { get; } + /// + /// Whether gameplay is paused. + /// IBindable IsPaused { get; } + + /// + /// Whether the clock is currently rewinding. + /// + bool IsRewinding { get; } } } From de74c9eb8bb8c24c7a0959969fcb0d9986dacb97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:06:40 +0900 Subject: [PATCH 107/555] Fix `GameplaySampleTriggerSource` not handling rewinds correctly --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index 18d412ab44..fed262868d 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -85,6 +85,14 @@ namespace osu.Game.Rulesets.UI sound.Stop(); }); + protected override void Update() + { + base.Update(); + + if (gameplayClock?.IsRewinding == true) + mostValidObject = null; + } + protected HitObject? GetMostValidObject() { if (mostValidObject == null || isAlreadyHit(mostValidObject)) From f69f6adf670f29d8a299ce3ef01b5609a2960ae4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 16:15:42 +0900 Subject: [PATCH 108/555] Remove beatmap info wedge rotation animation It looks jank and also causes framebuffer overheads. But mostly because it looks jank. --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 5dd2486e1c..3605e3d706 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -76,14 +76,12 @@ namespace osu.Game.Screens.Select protected override void PopIn() { this.MoveToX(0, animation_duration, Easing.OutQuint); - this.RotateTo(0, animation_duration, Easing.OutQuint); this.FadeIn(transition_duration); } protected override void PopOut() { this.MoveToX(-100, animation_duration, Easing.In); - this.RotateTo(10, animation_duration, Easing.In); this.FadeOut(transition_duration * 2, Easing.In); } From b40532dde167c056bccfde1c26d21ed7e348fdae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 17:17:16 +0900 Subject: [PATCH 109/555] Fix tournament bracket parsing regression Closes #24136. Regressed in #24037. --- osu.Game.Tournament/TournamentGameBase.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 7d0571dde0..29f0f4f356 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Input; @@ -94,7 +93,7 @@ namespace osu.Game.Tournament Task.Run(readBracket); } - private void readBracket() + private async Task readBracket() { try { @@ -102,7 +101,7 @@ namespace osu.Game.Tournament { using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) - ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter()); + ladder = JsonConvert.DeserializeObject(await sr.ReadToEndAsync().ConfigureAwait(false), new JsonPointConverter()); } ladder ??= new LadderInfo(); @@ -166,8 +165,8 @@ namespace osu.Game.Tournament } addedInfo |= addPlayers(); - addedInfo |= addRoundBeatmaps(); - addedInfo |= addSeedingBeatmaps(); + addedInfo |= await addRoundBeatmaps().ConfigureAwait(false); + addedInfo |= await addSeedingBeatmaps().ConfigureAwait(false); if (addedInfo) saveChanges(); @@ -233,7 +232,7 @@ namespace osu.Game.Tournament /// /// Add missing beatmap info based on beatmap IDs /// - private bool addRoundBeatmaps() + private async Task addRoundBeatmaps() { var beatmapsRequiringPopulation = ladder.Rounds .SelectMany(r => r.Beatmaps) @@ -246,7 +245,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false) ?? new APIBeatmap()); updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -257,7 +256,7 @@ namespace osu.Game.Tournament /// /// Add missing beatmap info based on beatmap IDs /// - private bool addSeedingBeatmaps() + private async Task addSeedingBeatmaps() { var beatmapsRequiringPopulation = ladder.Teams .SelectMany(r => r.SeedingResults) @@ -271,7 +270,7 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap()); + b.Beatmap = new TournamentBeatmap(await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false) ?? new APIBeatmap()); updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } From c9fd4354024b03fc1009388eb58853bc3f12de48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 17:58:27 +0900 Subject: [PATCH 110/555] Fix potential crash when mashing exit key --- osu.Game/Overlays/Dialog/PopupDialog.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 9969677826..e1e5604e4c 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -225,7 +225,12 @@ namespace osu.Game.Overlays.Dialog /// /// Programmatically clicks the first button of the provided type. /// - public void PerformAction() where T : PopupDialogButton => Buttons.OfType().First().TriggerClick(); + public void PerformAction() where T : PopupDialogButton + { + // Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure + // they are ready to be pressed. + Schedule(() => Buttons.OfType().First().TriggerClick()); + } protected override bool OnKeyDown(KeyDownEvent e) { From 7a3a14e50def1ee576a92c102b377ae8b86c2cd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 17:33:47 +0900 Subject: [PATCH 111/555] Add sample trigger tests covering rewinding of gameplay --- .../Gameplay/TestSceneGameplaySampleTriggerSource.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 6701871e8d..8938cff9a3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddStep("Add trigger source", () => Player.GameplayClockContainer.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); + AddStep("Add trigger source", () => Player.DrawableRuleset.FrameStableComponents.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); } [Test] @@ -153,6 +153,14 @@ namespace osu.Game.Tests.Visual.Gameplay waitForAliveObjectIndex(2); checkValidObjectIndex(2); + // test rewinding + seekBeforeIndex(1); + waitForAliveObjectIndex(1); + checkValidObjectIndex(1); + + seekBeforeIndex(1, 400); + checkValidObjectIndex(0); + seekBeforeIndex(3); waitForAliveObjectIndex(3); checkValidObjectIndex(3); From 2e98ab0a481f24502177011d2b31f29fc4697d75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:02:20 +0900 Subject: [PATCH 112/555] Expose rewinding state of `IGameplayClock`s The implementation of this requires a bit of a special case for 0, so makes sense to implement in a central place. --- .../Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs | 1 + osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 +++++ osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 5 +++++ osu.Game/Screens/Play/GameplayClockContainer.cs | 5 ++--- osu.Game/Screens/Play/IGameplayClock.cs | 8 ++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index bcb5291108..db06329d74 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -125,6 +125,7 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable NonGameplayAdjustments => throw new NotImplementedException(); public IBindable IsPaused => throw new NotImplementedException(); + public bool IsRewinding => false; } } } diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 080b0ce7ec..399e2fde7f 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -64,6 +64,8 @@ namespace osu.Game.Beatmaps [Resolved] private IBindable beatmap { get; set; } = null!; + public bool IsRewinding { get; private set; } + public bool IsCoupled { get => decoupledClock.IsCoupled; @@ -133,6 +135,9 @@ namespace osu.Game.Beatmaps } else finalClockSource.ProcessFrame(); + + if (Clock.ElapsedFrameTime != 0) + IsRewinding = Clock.ElapsedFrameTime < 0; } public double TotalAppliedOffset diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4bb145973d..90cffab714 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -171,6 +171,9 @@ namespace osu.Game.Rulesets.UI // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed framedClock.ProcessFrame(); + + if (framedClock.ElapsedFrameTime != 0) + IsRewinding = framedClock.ElapsedFrameTime < 0; } /// @@ -247,6 +250,8 @@ namespace osu.Game.Rulesets.UI public IBindable IsPaused { get; } = new BindableBool(); + public bool IsRewinding { get; private set; } + public double CurrentTime => framedClock.CurrentTime; public double Rate => framedClock.Rate; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c42f607908..22e6884526 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -19,11 +19,10 @@ namespace osu.Game.Screens.Play [Cached(typeof(IGameplayClock))] public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { - /// - /// Whether gameplay is paused. - /// public IBindable IsPaused => isPaused; + public bool IsRewinding => GameplayClock.IsRewinding; + /// /// The source clock. Should generally not be used for any timekeeping purposes. /// diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index 83ba5f3474..ad28e343ff 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -23,6 +23,14 @@ namespace osu.Game.Screens.Play /// IAdjustableAudioComponent AdjustmentsFromMods { get; } + /// + /// Whether gameplay is paused. + /// IBindable IsPaused { get; } + + /// + /// Whether the clock is currently rewinding. + /// + bool IsRewinding { get; } } } From 753db044b44bfcbec073637ae4273ba7bc84dd68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jul 2023 14:06:40 +0900 Subject: [PATCH 113/555] Fix `GameplaySampleTriggerSource` not handling rewinds correctly --- osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index c554318357..d6375ad282 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -69,6 +69,14 @@ namespace osu.Game.Rulesets.UI hitSound.Play(); }); + protected override void Update() + { + base.Update(); + + if (gameplayClock?.IsRewinding == true) + mostValidObject = null; + } + protected HitObject? GetMostValidObject() { if (mostValidObject == null || isAlreadyHit(mostValidObject)) From 070b3883ce534cf6d9c5a40f8a329ac47e2bff16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 00:31:32 +0900 Subject: [PATCH 114/555] Remove the ability to cancel all "in progress" tasks --- osu.Game/Localisation/NotificationsStrings.cs | 5 --- osu.Game/Overlays/NotificationOverlay.cs | 4 +-- .../Notifications/NotificationSection.cs | 32 +++++++++---------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 44e440e8d9..53687f2b28 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -29,11 +29,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClearAll => new TranslatableString(getKey(@"clear_all"), @"Clear All"); - /// - /// "Cancel All" - /// - public static LocalisableString CancelAll => new TranslatableString(getKey(@"cancel_all"), @"Cancel All"); - /// /// "Your battery level is low! Charge your device to prevent interruptions during gameplay." /// diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 21027b0931..5c0427bf03 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -108,8 +108,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, NotificationsStrings.ClearAll), - new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }, NotificationsStrings.CancelAll), + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, true), + new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }, false), } } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 4e28ade802..168c6afe27 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Overlays.Notifications @@ -38,16 +39,16 @@ namespace osu.Game.Overlays.Notifications public IEnumerable AcceptedNotificationTypes { get; } - private readonly LocalisableString clearButtonText; - private readonly LocalisableString titleText; - public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, LocalisableString clearButtonText) + private readonly bool allowClear; + + public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, bool allowClear) { AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); - this.clearButtonText = clearButtonText.ToUpper(); titleText = title; + this.allowClear = allowClear; } [BackgroundDependencyLoader] @@ -71,15 +72,17 @@ namespace osu.Game.Overlays.Notifications { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Children = new[] { - new ClearAllButton - { - Text = clearButtonText, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Action = clearAll - }, + allowClear + ? new ClearAllButton + { + Text = NotificationsStrings.ClearAll, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Action = clearAll + } + : Empty(), new FillFlowContainer { Margin = new MarginPadding @@ -115,10 +118,7 @@ namespace osu.Game.Overlays.Notifications }); } - private void clearAll() - { - notifications.Children.ForEach(c => c.Close(true)); - } + private void clearAll() => notifications.Children.ForEach(c => c.Close(true)); protected override void Update() { From fdb572fdea7dd42d5385a2367ef334a79c0a36e8 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 5 Jul 2023 17:25:16 +0900 Subject: [PATCH 115/555] Add more/different notification sounds --- osu.Game/Online/Chat/MessageNotifier.cs | 2 ++ osu.Game/Overlays/Notifications/Notification.cs | 2 +- .../Overlays/Notifications/ProgressCompletionNotification.cs | 2 ++ osu.Game/Overlays/Notifications/SimpleErrorNotification.cs | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index ae249d1b7f..65aac723da 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -171,6 +171,8 @@ namespace osu.Game.Online.Chat public abstract partial class HighlightMessageNotification : SimpleNotification { + public override string PopInSampleName => "UI/notification-mention"; + protected HighlightMessageNotification(Message message, Channel channel) { this.message = message; diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 77d3317b1f..e2c6de0635 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool DisplayOnTop => true; - public virtual string PopInSampleName => "UI/notification-pop-in"; + public virtual string PopInSampleName => "UI/notification-default"; protected NotificationLight Light; diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs index 46972d4b5e..93286d9d36 100644 --- a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs @@ -10,6 +10,8 @@ namespace osu.Game.Overlays.Notifications { public partial class ProgressCompletionNotification : SimpleNotification { + public override string PopInSampleName => "UI/notification-done"; + public ProgressCompletionNotification() { Icon = FontAwesome.Solid.Check; diff --git a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs index 758eea93d4..81e3b40ffc 100644 --- a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs @@ -7,7 +7,7 @@ namespace osu.Game.Overlays.Notifications { public partial class SimpleErrorNotification : SimpleNotification { - public override string PopInSampleName => "UI/error-notification-pop-in"; + public override string PopInSampleName => "UI/notification-error"; public SimpleErrorNotification() { From 4ff4c3a12e271219d8f02d4cec539316bdc21480 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 5 Jul 2023 17:26:20 +0900 Subject: [PATCH 116/555] Remove sound from notification closing/hiding --- osu.Game/Overlays/NotificationOverlay.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 21027b0931..ceb5bdd1e9 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -229,14 +229,7 @@ namespace osu.Game.Overlays mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } - private void notificationClosed() => Schedule(() => - { - updateCounts(); - - // this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it. - // popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment. - playDebouncedSample("UI/overlay-pop-out"); - }); + private void notificationClosed() => Schedule(updateCounts); private void playDebouncedSample(string sampleName) { From 6e2b7f433b6e26d877c5d25a3db2987c7b88d8b4 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 5 Jul 2023 17:27:42 +0900 Subject: [PATCH 117/555] Add a sound for 'cancelling' `ProgessNotification` --- osu.Game/Overlays/Notifications/ProgressNotification.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index e6662e2179..f3eba37517 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -4,6 +4,8 @@ using System; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -122,6 +124,7 @@ namespace osu.Game.Overlays.Notifications cancellationTokenSource.Cancel(); IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration); + cancelSample?.Play(); loadingSpinner.Hide(); var icon = new SpriteIcon @@ -190,6 +193,8 @@ namespace osu.Game.Overlays.Notifications private LoadingSpinner loadingSpinner = null!; + private Sample? cancelSample; + private readonly TextFlowContainer textDrawable; public ProgressNotification() @@ -217,7 +222,7 @@ namespace osu.Game.Overlays.Notifications } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audioManager) { colourQueued = colours.YellowDark; colourActive = colours.Blue; @@ -236,6 +241,8 @@ namespace osu.Game.Overlays.Notifications Size = new Vector2(loading_spinner_size), } }); + + cancelSample = audioManager.Samples.Get(@"UI/notification-cancel"); } public override void Close(bool runFlingAnimation) From d4f5d0c8787088c31753fc054a5e96249c0736ca Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Jul 2023 01:06:09 +0900 Subject: [PATCH 118/555] Revert "Remove sound from notification closing/hiding" This reverts commit 244f3c6098bb27b66f5ff7fb8c76f38f56cfb4cd. --- osu.Game/Overlays/NotificationOverlay.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index ceb5bdd1e9..21027b0931 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -229,7 +229,14 @@ namespace osu.Game.Overlays mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } - private void notificationClosed() => Schedule(updateCounts); + private void notificationClosed() => Schedule(() => + { + updateCounts(); + + // this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it. + // popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment. + playDebouncedSample("UI/overlay-pop-out"); + }); private void playDebouncedSample(string sampleName) { From a55ba963a92b39bdbb80a7c79403e2b59cde1462 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Jul 2023 01:50:15 +0900 Subject: [PATCH 119/555] Don't play 'popout' sample when `ProgressNotification` completes --- osu.Game/Overlays/NotificationOverlay.cs | 9 ++++++--- osu.Game/Overlays/Notifications/Notification.cs | 1 + osu.Game/Overlays/Notifications/ProgressNotification.cs | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 21027b0931..d21ad625c1 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -169,7 +169,7 @@ namespace osu.Game.Overlays Logger.Log($"⚠️ {notification.Text}"); - notification.Closed += notificationClosed; + notification.Closed += () => notificationClosed(notification); if (notification is IHasCompletionTarget hasCompletionTarget) hasCompletionTarget.CompletionTarget = Post; @@ -229,17 +229,20 @@ namespace osu.Game.Overlays mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } - private void notificationClosed() => Schedule(() => + private void notificationClosed(Notification notification) => Schedule(() => { updateCounts(); // this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it. // popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment. - playDebouncedSample("UI/overlay-pop-out"); + playDebouncedSample(notification.PopOutSampleName); }); private void playDebouncedSample(string sampleName) { + if (string.IsNullOrEmpty(sampleName)) + return; + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) { audio.Samples.Get(sampleName)?.Play(); diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index e2c6de0635..8cdc373417 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -51,6 +51,7 @@ namespace osu.Game.Overlays.Notifications public virtual bool DisplayOnTop => true; public virtual string PopInSampleName => "UI/notification-default"; + public virtual string PopOutSampleName => "UI/overlay-pop-out"; protected NotificationLight Light; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index f3eba37517..53ac490297 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -29,6 +29,8 @@ namespace osu.Game.Overlays.Notifications protected override bool AllowFlingDismiss => false; + public override string PopOutSampleName => State is ProgressNotificationState.Cancelled ? base.PopOutSampleName : ""; + /// /// The function to post completion notifications back to. /// From f3f4bb635680b28c6f47090d2cbc2660992362f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jul 2023 20:56:24 +0200 Subject: [PATCH 120/555] Add one more safety against processing scores with missing beatmap --- osu.Game/BackgroundBeatmapProcessor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs index 9c140bdda9..323e783d8d 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/BackgroundBeatmapProcessor.cs @@ -208,7 +208,9 @@ 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.TotalScoreVersion == 30000002).AsEnumerable().Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All() + .Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) + .AsEnumerable().Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); From 1473abd909691d950171bd18be9dd1d639504f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jul 2023 22:01:02 +0200 Subject: [PATCH 121/555] Remove outdated xmldoc --- osu.Game/Database/RealmObjectExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 77d199cf9c..72529ed9ff 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -282,8 +282,6 @@ namespace osu.Game.Database /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . - /// - /// May be null in the case the provided collection is not managed. /// /// /// From ae2896ba7e9122d0ac3cd428d993430d1e18dcd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jul 2023 22:08:48 +0200 Subject: [PATCH 122/555] Sprinkle some more null-forgiving operators --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 2 +- osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index f4467867db..e2774cef00 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Database realm.Run(innerRealm => { - var binding = innerRealm.ResolveReference(tsr); + var binding = innerRealm.ResolveReference(tsr)!; innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); }); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index b04e514ec2..725925c8cf 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -440,7 +440,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } private void updateStoreFromButton(KeyButton button) => - realm.WriteAsync(r => r.Find(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString); + realm.WriteAsync(r => r.Find(button.KeyBinding.ID)!.KeyCombinationString = button.KeyBinding.KeyCombinationString); private void updateIsDefaultValue() { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 55bcb9f79d..31b5bd8365 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -141,7 +141,7 @@ namespace osu.Game.Scoring { Realm.Run(r => { - var beatmapScores = r.Find(beatmap.ID).Scores.ToList(); + var beatmapScores = r.Find(beatmap.ID)!.Scores.ToList(); Delete(beatmapScores, silent); }); } From 8978f2ddd8c5cc366481dedde5a1a036abf383e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 10:09:24 +0900 Subject: [PATCH 123/555] Remove all usage of `!something!.something` aka don't mix NRT forgiving syntax with not syntax --- .../SongSelect/TestScenePlaySongSelect.cs | 20 ++++++++++--------- osu.Game/Database/RealmLive.cs | 3 ++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index af3a6e178c..0bff40f258 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Enter); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection changed", () => selected != Beatmap.Value); } @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Down); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); } @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Enter); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection changed", () => selected != Beatmap.Value); } @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.ReleaseButton(MouseButton.Left); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); } @@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("return", () => songSelect!.MakeCurrent()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); @@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("update beatmap", () => { @@ -1011,7 +1011,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); }); - AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); @@ -1040,7 +1040,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap())); }); - AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); @@ -1161,6 +1161,8 @@ namespace osu.Game.Tests.Visual.SongSelect rulesets.Dispose(); } + private void waitForDismissed() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen()); + private partial class TestSongSelect : PlaySongSelect { public Action? StartRequested; diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index d96ccd9dcd..509fabec59 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Development; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Statistics; using Realms; @@ -133,7 +134,7 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - if (dataIsFromUpdateThread && !data.Realm!.IsClosed) + if (dataIsFromUpdateThread && !data.Realm.AsNonNull().IsClosed) { RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++; return; From d93548f4ea9a759faa6286e5814d54259f75d709 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 13:10:49 +0900 Subject: [PATCH 124/555] Add back "clear all" button for progress notifications but only clear cancelled --- osu.Game/Overlays/INotificationOverlay.cs | 2 +- osu.Game/Overlays/NotificationOverlay.cs | 4 +-- .../Notifications/NotificationSection.cs | 29 +++++++++---------- .../Notifications/ProgressNotification.cs | 2 ++ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index c5ff10c619..7a44fd63ea 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 && p.State != ProgressNotificationState.Cancelled); + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => !p.CompletedOrCancelled); } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 5c0427bf03..1ea0e6bb2e 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -108,8 +108,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, true), - new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }, false), + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }), + new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }), } } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 168c6afe27..80bc02a594 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -41,14 +41,11 @@ namespace osu.Game.Overlays.Notifications private readonly LocalisableString titleText; - private readonly bool allowClear; - - public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, bool allowClear) + public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes) { AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); titleText = title; - this.allowClear = allowClear; } [BackgroundDependencyLoader] @@ -72,17 +69,15 @@ namespace osu.Game.Overlays.Notifications { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new[] + Children = new Drawable[] { - allowClear - ? new ClearAllButton - { - Text = NotificationsStrings.ClearAll, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Action = clearAll - } - : Empty(), + new ClearAllButton + { + Text = NotificationsStrings.ClearAll, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Action = clearAll + }, new FillFlowContainer { Margin = new MarginPadding @@ -118,7 +113,11 @@ namespace osu.Game.Overlays.Notifications }); } - private void clearAll() => notifications.Children.ForEach(c => c.Close(true)); + private void clearAll() => notifications.Children.ForEach(c => + { + if (c is not ProgressNotification p || p.CompletedOrCancelled) + c.Close(true); + }); protected override void Update() { diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index e6662e2179..5df4c61fd7 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Notifications public Func? CancelRequested { get; set; } + public bool CompletedOrCancelled => State == ProgressNotificationState.Completed || State == ProgressNotificationState.Cancelled; + protected override bool AllowFlingDismiss => false; /// From f8be6d41f71995e1705d0b9a192e162197f2fb37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 13:23:45 +0900 Subject: [PATCH 125/555] Play basic notification test first --- .../TestSceneNotificationOverlay.cs | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 3cd5daf7a1..4d3ae079e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -52,6 +52,32 @@ namespace osu.Game.Tests.Visual.UserInterface notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"unread count: {count.NewValue}"; }; }); + [Test] + public void TestBasicFlow() + { + setState(Visibility.Visible); + AddStep(@"simple #1", sendHelloNotification); + AddStep(@"simple #2", sendAmazingNotification); + AddStep(@"progress #1", sendUploadProgress); + AddStep(@"progress #2", sendDownloadProgress); + + checkProgressingCount(2); + + setState(Visibility.Hidden); + + AddRepeatStep(@"add many simple", sendManyNotifications, 3); + + waitForCompletion(); + + AddStep(@"progress #3", sendUploadProgress); + + checkProgressingCount(1); + + checkDisplayedCount(33); + + waitForCompletion(); + } + [Test] public void TestForwardWithFlingRight() { @@ -411,32 +437,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for update applied", () => applyUpdate); } - [Test] - public void TestBasicFlow() - { - setState(Visibility.Visible); - AddStep(@"simple #1", sendHelloNotification); - AddStep(@"simple #2", sendAmazingNotification); - AddStep(@"progress #1", sendUploadProgress); - AddStep(@"progress #2", sendDownloadProgress); - - checkProgressingCount(2); - - setState(Visibility.Hidden); - - AddRepeatStep(@"add many simple", sendManyNotifications, 3); - - waitForCompletion(); - - AddStep(@"progress #3", sendUploadProgress); - - checkProgressingCount(1); - - checkDisplayedCount(33); - - waitForCompletion(); - } - [Test] public void TestImportantWhileClosed() { From 04a15502152e90e603237f232cd53218e2dadc62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 13:32:15 +0900 Subject: [PATCH 126/555] Redesign "local input" toggle in manual input tests to be more user-friendly - Only displays when required (there's literally zero case we want to return input to the test, as this is automatic on next action) - No longer hugs the right side of the screen (blocking visibility of some tests). --- .../Visual/OsuManualInputManagerTestScene.cs | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 16496ff320..d9c8170a33 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Framework.Testing.Input; +using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; @@ -25,9 +26,10 @@ namespace osu.Game.Tests.Visual protected readonly ManualInputManager InputManager; - private readonly RoundedButton buttonTest; private readonly RoundedButton buttonLocal; + private readonly Container takeControlOverlay; + /// /// Whether to create a nested container to handle s that result from local (manual) test input. /// This should be disabled when instantiating an instance else actions will be lost. @@ -66,12 +68,12 @@ namespace osu.Game.Tests.Visual UseParentInput = true, Child = mainContent }, - new Container + takeControlOverlay = new Container { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Margin = new MarginPadding(5), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding(40), CornerRadius = 5, Masking = true, Children = new Drawable[] @@ -80,28 +82,28 @@ namespace osu.Game.Tests.Visual { Colour = Color4.Black, RelativeSizeAxes = Axes.Both, - Alpha = 0.5f, + Alpha = 0.4f, }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Margin = new MarginPadding(5), - Spacing = new Vector2(5), + Margin = new MarginPadding(10), + Spacing = new Vector2(10), Children = new Drawable[] { new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Input Priority" + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = "The test is currently overriding local input", }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Margin = new MarginPadding(5), Spacing = new Vector2(5), Direction = FillDirection.Horizontal, @@ -109,15 +111,9 @@ namespace osu.Game.Tests.Visual { buttonLocal = new RoundedButton { - Text = "local", - Size = new Vector2(50, 30), - Action = returnUserInput - }, - buttonTest = new RoundedButton - { - Text = "test", - Size = new Vector2(50, 30), - Action = returnTestInput + Text = "Take control back", + Size = new Vector2(180, 30), + Action = () => InputManager.UseParentInput = true }, } }, @@ -128,6 +124,13 @@ namespace osu.Game.Tests.Visual }); } + protected override void Update() + { + base.Update(); + + takeControlOverlay.Alpha = InputManager.UseParentInput ? 0 : 1; + } + /// /// Wait for a button to become enabled, then click it. /// @@ -146,19 +149,5 @@ namespace osu.Game.Tests.Visual InputManager.Click(MouseButton.Left); }); } - - protected override void Update() - { - base.Update(); - - buttonTest.Enabled.Value = InputManager.UseParentInput; - buttonLocal.Enabled.Value = !InputManager.UseParentInput; - } - - private void returnUserInput() => - InputManager.UseParentInput = true; - - private void returnTestInput() => - InputManager.UseParentInput = false; } } From d4c252ddf97c7edf9af02ab66d6a1ec78d7a74d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:28:57 +0900 Subject: [PATCH 127/555] Revert cancelling logic changes based on review feedback --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 7144fd8ca5..5c9a57724f 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -132,12 +132,14 @@ namespace osu.Game.Rulesets.Taiko.UI { leftCentreTrigger.StopAllPlayback(); rightCentreTrigger.StopAllPlayback(); + strongCentreTrigger.StopAllPlayback(); } private void flushRimTriggerSources() { leftRimTrigger.StopAllPlayback(); rightRimTrigger.StopAllPlayback(); + strongCentreTrigger.StopAllPlayback(); } public void OnReleased(KeyBindingReleaseEvent e) From 48f27ff340f3a417af63b4e323c9f9e2f9115e8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:30:31 +0900 Subject: [PATCH 128/555] Move trigger source to own file Having such a large nested class inside a small top level class is VERY confusing. --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 118 ---------------- .../Argon/ArgonDrumSampleTriggerSource.cs | 129 ++++++++++++++++++ 2 files changed, 129 insertions(+), 118 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 91393d99fe..7ce020f291 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -1,11 +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 System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Game.Audio; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -25,120 +22,5 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); - - public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource - { - private readonly HitObjectContainer hitObjectContainer; - - [Resolved] - private ISkinSource skinSource { get; set; } = null!; - - /// - /// The minimum time to leave between flourishes that are added to strong rim hits. - /// - private const double time_between_flourishes = 2000; - - public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) - : base(hitObjectContainer, balance) - { - this.hitObjectContainer = hitObjectContainer; - } - - public override void Play(HitType hitType, bool strong) - { - TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; - - if (hitObject == null) - return; - - var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); - - // If the sample is provided by a legacy skin, we should not try and do anything special. - if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) - { - base.Play(hitType, strong); - return; - } - - // let the magic begin... - var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; - - if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) - samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); - - PlaySamples(samplesToPlay.ToArray()); - } - - private bool canPlayFlourish(TaikoHitObject hitObject) - { - double? lastFlourish = null; - - var hitObjects = hitObjectContainer.AliveObjects - .Reverse() - .Select(d => d.HitObject) - .OfType() - .Where(h => h.IsStrong && h.Type == HitType.Rim); - - // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). - // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to - // end of groups/combos of strong rim hits instead of the start. - foreach (var h in hitObjects) - { - bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; - - if (canFlourish) - lastFlourish = h.StartTime; - - if (h == hitObject) - return canFlourish; - } - - return false; - } - - public class VolumeAwareHitSampleInfo : HitSampleInfo - { - public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; - public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; - - public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) - : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) - { - } - - public override IEnumerable LookupNames - { - get - { - foreach (string name in base.LookupNames) - yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); - } - } - - private static string getBank(string originalBank, string sampleName, int volume) - { - // So basically we're overwriting mapper's bank intentions here. - // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. - - switch (sampleName) - { - case HIT_NORMAL: - case HIT_CLAP: - { - if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) - return BANK_DRUM; - - if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) - return BANK_NORMAL; - - return BANK_SOFT; - } - - default: - return originalBank; - } - } - } - } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs new file mode 100644 index 0000000000..3454b0fd11 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource + { + private readonly HitObjectContainer hitObjectContainer; + + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + + /// + /// The minimum time to leave between flourishes that are added to strong rim hits. + /// + private const double time_between_flourishes = 2000; + + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) + : base(hitObjectContainer, balance) + { + this.hitObjectContainer = hitObjectContainer; + } + + public override void Play(HitType hitType, bool strong) + { + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) + { + base.Play(hitType, strong); + return; + } + + // let the magic begin... + var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; + + if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) + samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + + PlaySamples(samplesToPlay.ToArray()); + } + + private bool canPlayFlourish(TaikoHitObject hitObject) + { + double? lastFlourish = null; + + var hitObjects = hitObjectContainer.AliveObjects + .Reverse() + .Select(d => d.HitObject) + .OfType() + .Where(h => h.IsStrong && h.Type == HitType.Rim); + + // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). + // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to + // end of groups/combos of strong rim hits instead of the start. + foreach (var h in hitObjects) + { + bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; + + if (canFlourish) + lastFlourish = h.StartTime; + + if (h == hitObject) + return canFlourish; + } + + return false; + } + + public class VolumeAwareHitSampleInfo : HitSampleInfo + { + public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; + public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; + + public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) + : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) + { + } + + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); + } + } + + private static string getBank(string originalBank, string sampleName, int volume) + { + // So basically we're overwriting mapper's bank intentions here. + // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. + + switch (sampleName) + { + case HIT_NORMAL: + case HIT_CLAP: + { + if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) + return BANK_DRUM; + + if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) + return BANK_NORMAL; + + return BANK_SOFT; + } + + default: + return originalBank; + } + } + } + } +} From 6bfbcca2fdf0a1b1bf6342b852aad99ec2ce4788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:44:44 +0900 Subject: [PATCH 129/555] Move `VolumeAwareHitSampleInfo` to own file --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 6 +-- .../Argon/ArgonDrumSampleTriggerSource.cs | 44 ---------------- .../Argon/VolumeAwareHitSampleInfo.cs | 52 +++++++++++++++++++ 3 files changed, 55 insertions(+), 47 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 7ce020f291..898753b568 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon private void load(IPooledSampleProvider sampleProvider) { // Warm up pools for non-standard samples. - sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); - sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); - sampleProvider.GetPooledSample(new ArgonDrumSampleTriggerSource.VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); } protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs index 3454b0fd11..958d4e3aec 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs @@ -81,49 +81,5 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return false; } - - public class VolumeAwareHitSampleInfo : HitSampleInfo - { - public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; - public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; - - public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) - : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) - { - } - - public override IEnumerable LookupNames - { - get - { - foreach (string name in base.LookupNames) - yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); - } - } - - private static string getBank(string originalBank, string sampleName, int volume) - { - // So basically we're overwriting mapper's bank intentions here. - // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. - - switch (sampleName) - { - case HIT_NORMAL: - case HIT_CLAP: - { - if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) - return BANK_DRUM; - - if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) - return BANK_NORMAL; - - return BANK_SOFT; - } - - default: - return originalBank; - } - } - } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs new file mode 100644 index 0000000000..3ca4b5a3c7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs @@ -0,0 +1,52 @@ +// 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.Audio; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class VolumeAwareHitSampleInfo : HitSampleInfo + { + public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; + public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; + + public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) + : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) + { + } + + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); + } + } + + private static string getBank(string originalBank, string sampleName, int volume) + { + // So basically we're overwriting mapper's bank intentions here. + // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. + + switch (sampleName) + { + case HIT_NORMAL: + case HIT_CLAP: + { + if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) + return BANK_DRUM; + + if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) + return BANK_NORMAL; + + return BANK_SOFT; + } + + default: + return originalBank; + } + } + } +} From 9bdc80a74934b781216ff924d5ccca79068e467e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:46:36 +0900 Subject: [PATCH 130/555] Move flourish playback to own trigger source --- .../Skinning/Argon/ArgonDrumSamplePlayer.cs | 18 ++++- .../Argon/ArgonDrumSampleTriggerSource.cs | 39 ---------- .../Argon/ArgonFlourishTriggerSource.cs | 76 +++++++++++++++++++ .../UI/DrumSamplePlayer.cs | 5 +- 4 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs index 898753b568..2ff36ef9bf 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -11,16 +12,31 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer { + private ArgonFlourishTriggerSource argonFlourishTrigger = null!; + [BackgroundDependencyLoader] - private void load(IPooledSampleProvider sampleProvider) + private void load(Playfield playfield, IPooledSampleProvider sampleProvider) { + var hitObjectContainer = playfield.HitObjectContainer; + // Warm up pools for non-standard samples. sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + + // We want to play back flourishes in an isolated source as to not have them cancelled. + AddInternal(argonFlourishTrigger = new ArgonFlourishTriggerSource(hitObjectContainer)); } protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); + + protected override void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) + { + base.Play(triggerSource, hitType, strong); + + // This won't always play something, but the logic for flourish playback is contained within. + argonFlourishTrigger.Play(hitType, strong); + } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs index 958d4e3aec..fb4c1067e3 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; @@ -14,20 +13,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource { - private readonly HitObjectContainer hitObjectContainer; - [Resolved] private ISkinSource skinSource { get; set; } = null!; - /// - /// The minimum time to leave between flourishes that are added to strong rim hits. - /// - private const double time_between_flourishes = 2000; - public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) : base(hitObjectContainer, balance) { - this.hitObjectContainer = hitObjectContainer; } public override void Play(HitType hitType, bool strong) @@ -49,37 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // let the magic begin... var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; - if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) - samplesToPlay.Add(new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); - PlaySamples(samplesToPlay.ToArray()); } - - private bool canPlayFlourish(TaikoHitObject hitObject) - { - double? lastFlourish = null; - - var hitObjects = hitObjectContainer.AliveObjects - .Reverse() - .Select(d => d.HitObject) - .OfType() - .Where(h => h.IsStrong && h.Type == HitType.Rim); - - // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). - // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to - // end of groups/combos of strong rim hits instead of the start. - foreach (var h in hitObjects) - { - bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; - - if (canFlourish) - lastFlourish = h.StartTime; - - if (h == hitObject) - return canFlourish; - } - - return false; - } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs new file mode 100644 index 0000000000..661f737843 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + internal partial class ArgonFlourishTriggerSource : ArgonDrumSampleTriggerSource + { + private readonly HitObjectContainer hitObjectContainer; + + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + + /// + /// The minimum time to leave between flourishes that are added to strong rim hits. + /// + private const double time_between_flourishes = 2000; + + public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer, SampleBalance.Centre) + { + this.hitObjectContainer = hitObjectContainer; + } + + public override void Play(HitType hitType, bool strong) + { + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) + return; + + if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) + PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true) }); + } + + private bool canPlayFlourish(TaikoHitObject hitObject) + { + double? lastFlourish = null; + + var hitObjects = hitObjectContainer.AliveObjects + .Reverse() + .Select(d => d.HitObject) + .OfType() + .Where(h => h.IsStrong && h.Type == HitType.Rim); + + // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). + // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to + // end of groups/combos of strong rim hits instead of the start. + foreach (var h in hitObjects) + { + bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; + + if (canFlourish) + lastFlourish = h.StartTime; + + if (h == hitObject) + return canFlourish; + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 5c9a57724f..310a4c1edb 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - triggerSource.Play(hitType, strong); + Play(triggerSource, hitType, strong); lastHitTime = Time.Current; lastAction = e.Action; @@ -101,6 +101,9 @@ namespace osu.Game.Rulesets.Taiko.UI return false; } + protected virtual void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) => + triggerSource.Play(hitType, strong); + private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions) { if (lastAction == null) From 755b82a30883d6922f1439bd6cb45383085d1946 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 14:54:49 +0900 Subject: [PATCH 131/555] Implement flourish trigger source via base class --- .../Skinning/Argon/ArgonFlourishTriggerSource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs index 661f737843..728977547c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs @@ -11,7 +11,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { - internal partial class ArgonFlourishTriggerSource : ArgonDrumSampleTriggerSource + internal partial class ArgonFlourishTriggerSource : DrumSampleTriggerSource { private readonly HitObjectContainer hitObjectContainer; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon private const double time_between_flourishes = 2000; public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer, SampleBalance.Centre) + : base(hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; } From 8b5d5c9ae2497a9dc24a932b65ec33db875fcdc0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:13:25 +0900 Subject: [PATCH 132/555] Fix rewinding causing incorrectly stronged non-strong hits --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 310a4c1edb..08bde9a316 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (lastAction == null) return false; - if (timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW) + if (timeBetweenActions < 0 || timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW) return false; switch (newAction) From e0fc97bb9302d3bb5d476b5af8936da1b91e13ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:21:24 +0900 Subject: [PATCH 133/555] Replace various local implementations of rewinding checks with new property --- .../Skinning/Legacy/LegacyCatchComboCounter.cs | 3 ++- .../Objects/Drawables/DrawableHoldNote.cs | 3 ++- .../Objects/Drawables/DrawableSliderBall.cs | 6 ++---- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 ++- osu.Game/Screens/Play/ComboEffects.cs | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index 55b24b3ffa..f38b9b430e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy lastDisplayedCombo = combo; - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) { // needs more work to make rewind somehow look good. // basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle). diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a8563d65c4..d5e212d389 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -298,7 +299,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return false; // do not run any of this logic when rewinding, as it inverts order of presses/releases. - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; if (CheckHittable?.Invoke(this, Time.Current) == false) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index e1766adc20..d06fb5b4de 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -179,16 +180,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Vector2? lastPosition; - private bool rewinding; - public void UpdateProgress(double completionProgress) { Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); - if (Clock.ElapsedFrameTime != 0) - rewinding = Clock.ElapsedFrameTime < 0; + bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true; // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. if (diff.LengthFast < 0.01f) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e4d8eb2335..bf649a0a15 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK.Graphics; @@ -688,7 +689,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected bool UpdateResult(bool userTriggered) { // It's possible for input to get into a bad state when rewinding gameplay, so results should not be processed - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; if (Judged) diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 09c94a8f1d..6f12cfde64 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play if (gameplayClock.CurrentTime < firstBreakTime) firstBreakTime = null; - if (gameplayClock.ElapsedFrameTime < 0) + if (gameplayClock.IsRewinding) return; if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlayFirst.Value && firstBreakTime == null))) From 67746e1369bacd864c7b107724416e656119eac4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:36:17 +0900 Subject: [PATCH 134/555] Move retry sample playback to `PlayerLoader` --- osu.Game/Screens/Play/Player.cs | 4 ---- osu.Game/Screens/Play/PlayerLoader.cs | 9 ++++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2cb7748a15..379c10a4a4 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -112,8 +112,6 @@ namespace osu.Game.Screens.Play private Ruleset ruleset; - private SkinnableSound sampleRestart; - public BreakOverlay BreakOverlay; /// @@ -303,7 +301,6 @@ namespace osu.Game.Screens.Play Restart(true); }, }, - sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }); } @@ -673,7 +670,6 @@ namespace osu.Game.Screens.Play // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. musicController.Stop(); - sampleRestart?.Play(); RestartRequested?.Invoke(quickRestart); PerformExit(false); diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 30ae5ee5aa..4b15bac0f3 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Audio.Effects; using osu.Game.Configuration; using osu.Game.Graphics; @@ -25,6 +26,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Skinning; using osu.Game.Users; using osu.Game.Utils; using osuTK; @@ -76,6 +78,8 @@ namespace osu.Game.Screens.Play private AudioFilter lowPassFilter = null!; private AudioFilter highPassFilter = null!; + private SkinnableSound sampleRestart = null!; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -199,7 +203,8 @@ namespace osu.Game.Screens.Play }, idleTracker = new IdleTracker(750), lowPassFilter = new AudioFilter(audio.TrackMixer), - highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass) + highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }; if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) @@ -265,6 +270,8 @@ namespace osu.Game.Screens.Play playerConsumed = false; cancelLoad(); + sampleRestart.Play(); + contentIn(); } From 9732e5733c2685f926e08a3300e246a2b32c59e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 15:45:21 +0900 Subject: [PATCH 135/555] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b4d8dd513f..d30398a475 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From ba0ab7383d051a074fc59fa2416f103093102e17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 16:28:49 +0900 Subject: [PATCH 136/555] Fix broken test nullability --- osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index f377d92911..d30b3c089e 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -383,7 +383,7 @@ namespace osu.Game.Tests.Database beatmapInfo.Hash = new_beatmap_hash; beatmapInfo.ResetOnlineInfo(); - beatmapInfo.UpdateLocalScores(s.Realm); + beatmapInfo.UpdateLocalScores(s.Realm!); }); realm.Run(r => r.Refresh()); From 82babbf8faee0876962df4f93f08443c91e6091f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 17:32:22 +0900 Subject: [PATCH 137/555] Adjust results screen transition tweens to feel better --- osu.Game/Screens/Ranking/ResultsScreen.cs | 8 ++++---- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index b9f3b65129..96fed3e6ba 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -305,7 +305,7 @@ namespace osu.Game.Screens.Ranking float origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos).X; expandedPanel.MoveToX(origLocation) .Then() - .MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); + .MoveToX(StatisticsPanel.SIDE_PADDING, 400, Easing.OutElasticQuarter); // Hide contracted panels. foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -313,7 +313,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = false; // Dim background. - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.1f), 150)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.4f), 400, Easing.OutQuint)); detachedPanel = expandedPanel; } @@ -329,7 +329,7 @@ namespace osu.Game.Screens.Ranking float origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos).X; detachedPanel.MoveToX(origLocation) .Then() - .MoveToX(0, 150, Easing.OutQuint); + .MoveToX(0, 250, Easing.OutElasticQuarter); // Show contracted panels. foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -337,7 +337,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = true; // Un-dim background. - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.5f), 150)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.5f), 250, Easing.OutQuint)); detachedPanel = null; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c36d7726dc..8b059efaf4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -223,7 +223,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopIn() { - this.FadeIn(150, Easing.OutQuint); + this.FadeIn(350, Easing.OutQuint); popInSample?.Play(); wasOpened = true; @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopOut() { - this.FadeOut(150, Easing.OutQuint); + this.FadeOut(250, Easing.OutQuint); if (wasOpened) popOutSample?.Play(); From 0e85a33ca2d6ab741566879c336246de01caaa71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 20:58:04 +0900 Subject: [PATCH 138/555] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index fdec4e575b..270a0aa44b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 20b1574617..184a77a286 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 86694e268a..51bcc36621 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 2cd5a4c6c26f5c6f0278a5aec17a6e20bdd28b89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 22:38:07 +0900 Subject: [PATCH 139/555] Add generated code hints in editorconfig / dotsettings --- .editorconfig | 3 +++ osu.sln.DotSettings | 1 + 2 files changed, 4 insertions(+) diff --git a/.editorconfig b/.editorconfig index 67c47000d3..c249e5e9b3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,9 @@ indent_style = space indent_size = 2 trim_trailing_whitespace = true +[g_*.cs] +generated_code = true + [*.cs] end_of_line = crlf insert_final_newline = true diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index b54794cd6d..482095db57 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -5,6 +5,7 @@ True ExplicitlyExcluded ExplicitlyExcluded + g_*.cs SOLUTION WARNING WARNING From a76cd9b0e6d579e4aa67e796f1d1cb7457f857f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Jul 2023 00:44:26 +0900 Subject: [PATCH 140/555] Update osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs Co-authored-by: Jamie Taylor --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 08bde9a316..93d2406c0c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Taiko.UI { leftRimTrigger.StopAllPlayback(); rightRimTrigger.StopAllPlayback(); - strongCentreTrigger.StopAllPlayback(); + strongRimTrigger.StopAllPlayback(); } public void OnReleased(KeyBindingReleaseEvent e) From 9dae80673474d40902c76f2149afa378d01e7699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 13:31:21 +0200 Subject: [PATCH 141/555] Use `IsRewinding` in a few more places --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 3 ++- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 1b99270b65..567c288b47 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Catch.UI @@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.X = Catcher.X; - if (Time.Elapsed <= 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) { // This is probably a wrong value, but currently the true value is not recorded. // Setting `true` will prevent generation of false-positive after-images (with more false-negatives). diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index d5e212d389..c3fec92b92 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -338,7 +338,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; // do not run any of this logic when rewinding, as it inverts order of presses/releases. - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return; Tail.UpdateResult(); From 3a9b259f8a34100ab4ef796cfa3f99aef925129e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 14:10:05 +0200 Subject: [PATCH 142/555] Add back removed `.ToUpper()` case transform --- osu.Game/Overlays/Notifications/NotificationSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 80bc02a594..be57d23446 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays.Notifications { new ClearAllButton { - Text = NotificationsStrings.ClearAll, + Text = NotificationsStrings.ClearAll.ToUpper(), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Action = clearAll From cdaf8e4b0ff53937209603f30d8b46b290dac83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 14:11:58 +0200 Subject: [PATCH 143/555] Flip and rename `CompletedOrCancelled` to `Ongoing` --- osu.Game/Overlays/INotificationOverlay.cs | 4 ++-- osu.Game/Overlays/Notifications/NotificationSection.cs | 2 +- osu.Game/Overlays/Notifications/ProgressNotification.cs | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index 7a44fd63ea..19c646a714 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -42,8 +42,8 @@ namespace osu.Game.Overlays IEnumerable AllNotifications { get; } /// - /// All ongoing operations (ie. any not in a completed state). + /// All ongoing operations (ie. any not in a completed or cancelled state). /// - public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => !p.CompletedOrCancelled); + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.Ongoing); } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index be57d23446..10c2900d63 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Notifications private void clearAll() => notifications.Children.ForEach(c => { - if (c is not ProgressNotification p || p.CompletedOrCancelled) + if (c is not ProgressNotification p || !p.Ongoing) c.Close(true); }); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 5df4c61fd7..466dfab5c5 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -25,7 +25,10 @@ namespace osu.Game.Overlays.Notifications public Func? CancelRequested { get; set; } - public bool CompletedOrCancelled => State == ProgressNotificationState.Completed || State == ProgressNotificationState.Cancelled; + /// + /// Whether the operation represented by the is still ongoing. + /// + public bool Ongoing => State != ProgressNotificationState.Completed && State != ProgressNotificationState.Cancelled; protected override bool AllowFlingDismiss => false; From 0ecfb7b36fbf6389f433ff2234a92936b24d3eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 15:03:33 +0200 Subject: [PATCH 144/555] Remove unused field --- osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index d9c8170a33..37260b3b13 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -26,8 +26,6 @@ namespace osu.Game.Tests.Visual protected readonly ManualInputManager InputManager; - private readonly RoundedButton buttonLocal; - private readonly Container takeControlOverlay; /// @@ -109,7 +107,7 @@ namespace osu.Game.Tests.Visual Children = new Drawable[] { - buttonLocal = new RoundedButton + new RoundedButton { Text = "Take control back", Size = new Vector2(180, 30), From fee56ac6d2ece7d897a6419d0bfbc9ee65e95c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 18:29:49 +0200 Subject: [PATCH 145/555] Use new `IGameplayClock.IsRewinding` member --- osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 93d2406c0c..57067ac666 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -9,6 +9,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Taiko.UI { @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.UI public bool OnPressed(KeyBindingPressEvent e) { - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; HitType hitType; From ba0cd7a3f27206e5b815b709da91bf46969e3c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 19:22:32 +0200 Subject: [PATCH 146/555] Fix flourish sample not playing if strong hits are hit early --- .../Skinning/Argon/ArgonFlourishTriggerSource.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs index 728977547c..8dfe31b55d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs @@ -66,7 +66,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon if (canFlourish) lastFlourish = h.StartTime; - if (h == hitObject) + // hitObject can be either the strong hit itself (if hit late), or its nested strong object (if hit early) + // due to `GetMostValidObject()` idiosyncrasies. + // whichever it is, if we encounter it during iteration, stop looking. + if (h == hitObject || h.NestedHitObjects.Contains(hitObject)) return canFlourish; } From e9ecad983932086155adb7bb2527ab0f5bf37f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 23:02:41 +0200 Subject: [PATCH 147/555] Add failing test cases covering NaN-timing-point sliders --- .../OsuBeatmapConversionTest.cs | 1 + .../OsuDifficultyCalculatorTest.cs | 1 + .../nan-slider-expected-conversion.json | 1 + .../Resources/Testing/Beatmaps/nan-slider.osu | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+) create mode 100755 osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json create mode 100755 osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index 4c11efcc7c..b94e9f38c6 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase("uneven-repeat-slider")] [TestCase("old-stacking")] [TestCase("multi-segment-slider")] + [TestCase("nan-slider")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index cda330afe5..9c6449cfa9 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(6.7115569159190587d, 206, "diffcalc-test")] [TestCase(1.4391311903612753d, 45, "zero-length-sliders")] + [TestCase(0.14102693012101306d, 1, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json new file mode 100755 index 0000000000..86a4a278f1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77497.0,"X":298.0,"Y":290.0},{"StartTime":77533.0,"EndTime":77533.0,"X":276.162567,"Y":293.0336}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu new file mode 100755 index 0000000000..fa545a7614 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu @@ -0,0 +1,18 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:5.8 +CircleSize:4 +OverallDifficulty:9.6 +ApproachRate:10 +SliderMultiplier:2 +SliderTickRate:1 + +[TimingPoints] +77211,-100,4,3,50,70,0,0 +77497,8.40402703648439,4,3,51,70,1,8 +77497,NaN,4,3,51,70,0,8 +77498,285.714285714286,4,3,51,70,1,0 + +[HitObjects] +298,290,77497,6,0,B|234:298|192:279|192:279|180:299|180:299|205:311|238:318|238:318|230:347|217:371|217:371|137:370|80:340|80:340|65:259|73:143|102:68|102:68|149:49|199:34|199:34|213:54|213:54|267:38|324:40|324:40|332:18|332:18|385:20|435:27|435:27|480:93|517:204|521:286|521:286|474:329|396:350|396:350|377:329|363:302|363:302|393:287|415:271|415:271|398:254|398:254|362:282|299:290,1,1723.66345596313,10|0,1:0|3:0,3:0:0:0: From 56a2ba4ac0291ade389d9dbfe5dbb801e70d04d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Jul 2023 23:08:36 +0200 Subject: [PATCH 148/555] Fix `GenerateTicks` being lost during osu! beatmap conversion process --- osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index e947690668..790af6cfc1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo, IHasGenerateTicks { public Vector2 Position { get; set; } @@ -20,5 +20,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } public int ComboOffset { get; set; } + + public bool GenerateTicks { get; set; } = true; } } From ae05df3b8c5d0f44bb1de43c3dbb8087d90adc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 15:56:51 +0200 Subject: [PATCH 149/555] Add `ModScoreV2` --- osu.Game/Rulesets/Mods/ModScoreV2.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 osu.Game/Rulesets/Mods/ModScoreV2.cs diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs new file mode 100644 index 0000000000..6d56b2d86f --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active. + /// It should not be used in any real capacity going forward. + /// + public class ModScoreV2 : Mod + { + public override string Name => "Score V2"; + public override string Acronym => @"SV2"; + public override ModType Type => ModType.System; + public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active."; + public override double ScoreMultiplier => 1; + } +} From 10ba04512d5963eb1ae6d028d83d65dd644d8ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:06:05 +0200 Subject: [PATCH 150/555] Add `ScoreV2` to `LegacyMods` --- osu.Game/Beatmaps/Legacy/LegacyMods.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 0e517ea3df..747015d90a 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps.Legacy Key1 = 1 << 26, Key3 = 1 << 27, Key2 = 1 << 28, + ScoreV2 = 1 << 29, Mirror = 1 << 30, } } From 2cd5fd5944e48e9625353e3295ec017e1399ab73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:09:25 +0200 Subject: [PATCH 151/555] Add failing legacy mod conversion test cases --- osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs | 4 +++- osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs | 4 +++- osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs | 4 +++- osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index b74120fa3c..dacfd649ef 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -5,6 +5,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests @@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(catch_mod_mapping))] diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 3a9639e04d..cb2abc1595 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -5,6 +5,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests @@ -36,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } }, new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(mania_mod_mapping))] diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index 05366e9444..2cf9842c83 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Beatmaps; @@ -28,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(osu_mod_mapping))] diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index 541987d63e..5f7a78ddf1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Beatmaps; @@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } }, new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(taiko_mod_mapping))] From 7be5e0e97832bfc8fdb4c5989839a820861d8a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:15:17 +0200 Subject: [PATCH 152/555] Implement back-and-forth conversion of `ModScoreV2` and `LegacyMods` --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 9 +++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 9 +++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 9 +++++++++ osu.Game/Rulesets/Ruleset.cs | 4 ++++ 5 files changed, 35 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 8f1a1b8ef5..e51e5cc5db 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -91,6 +91,9 @@ namespace osu.Game.Rulesets.Catch if (mods.HasFlagFast(LegacyMods.Relax)) yield return new CatchModRelax(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override IEnumerable GetModsFor(ModType type) @@ -140,6 +143,12 @@ namespace osu.Game.Rulesets.Catch new CatchModNoScope(), }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2e96c89516..bd6ab4086b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlagFast(LegacyMods.Mirror)) yield return new ManiaModMirror(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -285,6 +288,12 @@ namespace osu.Game.Rulesets.Mania new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b44d999d4f..036d13c5aa 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -113,6 +113,9 @@ namespace osu.Game.Rulesets.Osu if (mods.HasFlagFast(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -212,6 +215,7 @@ namespace osu.Game.Rulesets.Osu return new Mod[] { new OsuModTouchDevice(), + new ModScoreV2(), }; default: diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index aa31b1924f..de3fa1750f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -116,6 +116,9 @@ namespace osu.Game.Rulesets.Taiko if (mods.HasFlagFast(LegacyMods.Random)) yield return new TaikoModRandom(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -176,6 +179,12 @@ namespace osu.Game.Rulesets.Taiko new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 490ec1475c..cd432e050b 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -192,6 +192,10 @@ namespace osu.Game.Rulesets case ModAutoplay: value |= LegacyMods.Autoplay; break; + + case ModScoreV2: + value |= LegacyMods.ScoreV2; + break; } } From 9377622cd4010d7141eecaeebd87663fe50f358b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 16:36:51 +0200 Subject: [PATCH 153/555] Add backwards migration to populate ScoreV2 mod for already-imported scores --- osu.Game/Database/RealmAccess.cs | 67 +++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f9f11c49ff..1af0cf30ba 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -15,17 +15,20 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Input.Bindings; using osu.Game.IO.Legacy; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -79,8 +82,9 @@ namespace osu.Game.Database /// 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 copy TotalScore into LegacyTotalScore for legacy scores. + /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// - private const int schema_version = 31; + private const int schema_version = 32; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -730,6 +734,8 @@ namespace osu.Game.Database Logger.Log($"Running realm migration to version {targetVersion}..."); Stopwatch stopwatch = new Stopwatch(); + var files = new RealmFileStore(this, storage); + stopwatch.Start(); switch (targetVersion) @@ -904,7 +910,6 @@ namespace osu.Game.Database case 28: { - var files = new RealmFileStore(this, storage); var scores = migration.NewRealm.All(); foreach (var score in scores) @@ -986,6 +991,64 @@ namespace osu.Game.Database break; } + + case 32: + { + foreach (var score in migration.NewRealm.All()) + { + if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset()) + continue; + + string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); + if (replayFilename == null) + continue; + + try + { + using (var stream = files.Store.GetStream(replayFilename)) + { + if (stream == null) + continue; + + // Trimmed down logic from LegacyScoreDecoder to extract the mods bitmask. + using (SerializationReader sr = new SerializationReader(stream)) + { + sr.ReadByte(); // Ruleset. + sr.ReadInt32(); // Version. + sr.ReadString(); // Beatmap hash. + sr.ReadString(); // Username. + sr.ReadString(); // MD5Hash. + sr.ReadUInt16(); // Count300. + sr.ReadUInt16(); // Count100. + sr.ReadUInt16(); // Count50. + sr.ReadUInt16(); // CountGeki. + sr.ReadUInt16(); // CountKatu. + sr.ReadUInt16(); // CountMiss. + + // we should have this in LegacyTotalScore already, but if we're reading through this anyways... + int totalScore = sr.ReadInt32(); + + sr.ReadUInt16(); // Max combo. + sr.ReadBoolean(); // Perfect. + + var legacyMods = (LegacyMods)sr.ReadInt32(); + + if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + continue; + + score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); + score.LegacyTotalScore = score.TotalScore = totalScore; + } + } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); + } + } + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); From e12255bbe5e41a6cba7acb28282f74c008769575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 17:01:28 +0200 Subject: [PATCH 154/555] Do not run legacy conversion with ScoreV2 mod present --- 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 60530c31cb..bc3629c25b 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -205,6 +206,10 @@ namespace osu.Game.Database if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; + var mods = score.Mods; + if (mods.Any(mod => mod is ModScoreV2)) + return score.TotalScore; + var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); if (playableBeatmap.HitObjects.Count == 0) @@ -212,7 +217,7 @@ namespace osu.Game.Database ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); - sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods); + sv1Simulator.Simulate(beatmap, playableBeatmap, mods); return ConvertFromLegacyTotalScore(score, new DifficultyAttributes { From 45194b2b4a4ebb26ce3f1fc2e948f4d21495f828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 18:21:43 +0200 Subject: [PATCH 155/555] Fix pressing Ctrl-C in composer not copying timestamp to system clipboard --- .../Screens/Edit/Compose/ComposeScreen.cs | 25 +++++++++------- osu.Game/Screens/Edit/EditorScreen.cs | 29 +++++++------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index dc026f7eac..433fb5c8ee 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -101,26 +101,31 @@ namespace osu.Game.Screens.Edit.Compose #region Clipboard operations - protected override void PerformCut() + public override void Cut() { - base.PerformCut(); + if (!CanCut.Value) + return; Copy(); EditorBeatmap.RemoveRange(EditorBeatmap.SelectedHitObjects.ToArray()); } - protected override void PerformCopy() + public override void Copy() { - base.PerformCopy(); + // on stable, pressing Ctrl-C would copy the current timestamp to system clipboard + // regardless of whether anything was even selected at all. + // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. + // note that this means that `getTimestamp()` must handle no-selection case, too. + host.GetClipboard()?.SetText(getTimestamp()); - clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); - - host.GetClipboard()?.SetText(formatSelectionAsString()); + if (CanCopy.Value) + clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); } - protected override void PerformPaste() + public override void Paste() { - base.PerformPaste(); + if (!CanPaste.Value) + return; var objects = clipboard.Value.Deserialize().HitObjects; @@ -147,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose CanPaste.Value = composer.IsLoaded && !string.IsNullOrEmpty(clipboard.Value); } - private string formatSelectionAsString() + private string getTimestamp() { if (composer == null) return string.Empty; diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index b39c0cf5f3..3bc870b898 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -44,29 +44,23 @@ namespace osu.Game.Screens.Edit /// /// Performs a "cut to clipboard" operation appropriate for the given screen. /// - protected virtual void PerformCut() + /// + /// Implementors are responsible for checking themselves. + /// + public virtual void Cut() { } - public void Cut() - { - if (CanCut.Value) - PerformCut(); - } - public BindableBool CanCopy { get; } = new BindableBool(); /// /// Performs a "copy to clipboard" operation appropriate for the given screen. /// - protected virtual void PerformCopy() - { - } - + /// + /// Implementors are responsible for checking themselves. + /// public virtual void Copy() { - if (CanCopy.Value) - PerformCopy(); } public BindableBool CanPaste { get; } = new BindableBool(); @@ -74,14 +68,11 @@ namespace osu.Game.Screens.Edit /// /// Performs a "paste from clipboard" operation appropriate for the given screen. /// - protected virtual void PerformPaste() - { - } - + /// + /// Implementors are responsible for checking themselves. + /// public virtual void Paste() { - if (CanPaste.Value) - PerformPaste(); } #endregion From d135b3f6f57a6d3e67ec45b3a250bbf58451ae4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:27:33 +0200 Subject: [PATCH 156/555] Add message length limit field to API response --- osu.Game/Online/Chat/Channel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 761e8aba8d..3f43560f41 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -86,6 +86,9 @@ namespace osu.Game.Online.Chat [JsonProperty(@"last_read_id")] public long? LastReadId; + [JsonProperty(@"message_length_limit")] + public int MessageLengthLimit; + /// /// Signals if the current user joined this channel or not. Defaults to false. /// Note that this does not guarantee a join has completed. Check Id > 0 for confirmation. From 2af8c7bc24e3435dba1c7feb08bd1ee923bc9d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:35:24 +0200 Subject: [PATCH 157/555] Add failing test case for chat message length limit enforcement --- .../Visual/Online/TestSceneChatTextBox.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs index 1e80acd56b..8c5475223c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -3,11 +3,13 @@ #nullable disable +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; @@ -18,7 +20,7 @@ using osu.Game.Overlays.Chat; namespace osu.Game.Tests.Visual.Online { [TestFixture] - public partial class TestSceneChatTextBox : OsuTestScene + public partial class TestSceneChatTextBox : OsuManualInputManagerTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); @@ -30,6 +32,8 @@ namespace osu.Game.Tests.Visual.Online private OsuSpriteText searchText; private ChatTextBar bar; + private ChatTextBox textBox => bar.ChildrenOfType().Single(); + [SetUp] public void SetUp() { @@ -115,6 +119,36 @@ namespace osu.Game.Tests.Visual.Online AddStep("Chat Mode Search", () => bar.ShowSearch.Value = true); } + [Test] + public void TestLengthLimit() + { + var firstChannel = new Channel + { + Name = "#test1", + Type = ChannelType.Public, + Id = 4567, + MessageLengthLimit = 20 + }; + var secondChannel = new Channel + { + Name = "#test2", + Type = ChannelType.Public, + Id = 5678, + MessageLengthLimit = 5 + }; + + AddStep("switch to channel with 20 char length limit", () => currentChannel.Value = firstChannel); + AddStep("type a message", () => textBox.Current.Value = "abcdefgh"); + + AddStep("switch to channel with 5 char length limit", () => currentChannel.Value = secondChannel); + AddAssert("text box empty", () => textBox.Current.Value, () => Is.Empty); + AddStep("type too much", () => textBox.Current.Value = "123456"); + AddAssert("text box has 5 chars", () => textBox.Current.Value, () => Has.Length.EqualTo(5)); + + AddStep("switch back to channel with 20 char length limit", () => currentChannel.Value = firstChannel); + AddAssert("unsent message preserved without truncation", () => textBox.Current.Value, () => Is.EqualTo("abcdefgh")); + } + private static Channel createPublicChannel(string name) => new Channel { Name = name, Type = ChannelType.Public, Id = 1234 }; From 6453ab6049937e6e47d114ac5baa979771b369f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:42:13 +0200 Subject: [PATCH 158/555] Set chat text box message length limit based on channel --- osu.Game/Overlays/Chat/ChatTextBar.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 87e787fcb8..16a8d14b10 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -156,7 +156,11 @@ namespace osu.Game.Overlays.Chat chatTextBox.Current.UnbindFrom(change.OldValue.TextBoxMessage); if (newChannel != null) + { + // change length limit first before binding to avoid accidentally truncating pending message from new channel. + chatTextBox.LengthLimit = newChannel.MessageLengthLimit; chatTextBox.Current.BindTo(newChannel.TextBoxMessage); + } }, true); } From 91e286560ef53428cc6159e4df6b1c641e503c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 9 Jul 2023 21:49:50 +0200 Subject: [PATCH 159/555] Fix search being broken in channel listing "channel" --- osu.Game/Online/Chat/Channel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 3f43560f41..15ce926039 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Lists; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.Listing; namespace osu.Game.Online.Chat { @@ -86,8 +87,11 @@ namespace osu.Game.Online.Chat [JsonProperty(@"last_read_id")] public long? LastReadId; + /// + /// Purposefully nullable for the sake of . + /// [JsonProperty(@"message_length_limit")] - public int MessageLengthLimit; + public int? MessageLengthLimit; /// /// Signals if the current user joined this channel or not. Defaults to false. From 89b110e3aa318a16bab399df07408576ac9d9aae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jul 2023 21:26:20 +0900 Subject: [PATCH 160/555] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 270a0aa44b..759167829c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 184a77a286..7968364243 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 51bcc36621..2e691da079 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 9a2915f423fa03cbc3daf4a603c0db849f8693b7 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Mon, 10 Jul 2023 17:29:49 +0300 Subject: [PATCH 161/555] Add beatmap minimum length checks --- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../Rulesets/Edit/Checks/CheckDrainTime.cs | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index dc73e35923..3988f29e13 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Edit new CheckUnsnappedObjects(), new CheckConcurrentObjects(), new CheckZeroLengthObjects(), + new CheckDrainTime(), // Timing new CheckPreviewTime(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs new file mode 100644 index 0000000000..99a74e6479 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs @@ -0,0 +1,38 @@ +// 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.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckDrainTime : ICheck + { + private const int min_drain_threshold = 30 * 1000; + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "Too short drain time"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + double drainTime = context.Beatmap.CalculatePlayableLength(); + + if (drainTime < min_drain_threshold) + yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000)); + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "Less than 30 seconds of drain time, currently {0}.") + { + } + + public Issue Create(int drainTimeSeconds) => new Issue(this, drainTimeSeconds); + } + } +} From c972a4195c93690d22427303c8840e411455c616 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Mon, 10 Jul 2023 17:29:59 +0300 Subject: [PATCH 162/555] Add tests --- .../Editing/Checks/CheckDrainTimeTest.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs new file mode 100644 index 0000000000..ffa3af8fc6 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + public class CheckDrainTimeTest + { + private CheckDrainTime check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckDrainTime(); + } + + [Test] + public void TestDrainTimeShort() + { + setShortDrainTimeBeatmap(); + var content = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(content).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); + } + + private void setShortDrainTimeBeatmap() + { + beatmap = new Beatmap + { + HitObjects = + { + new HitCircle() + } + }; + } + } +} From 9e4ffc8c12cdce5bb34fee06fafdec76ac41337e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Jul 2023 21:10:01 +0200 Subject: [PATCH 163/555] Split helper method for populations from replay --- osu.Game/Database/RealmAccess.cs | 93 ++++++------------- .../StandardisedScoreMigrationTools.cs | 35 +++++++ 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1af0cf30ba..f32b161bb6 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -26,7 +26,6 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Input.Bindings; -using osu.Game.IO.Legacy; using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -914,31 +913,13 @@ namespace osu.Game.Database foreach (var score in scores) { - string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); - if (replayFilename == null) - continue; - - try + score.PopulateFromReplay(files, sr => { - using (var stream = files.Store.GetStream(replayFilename)) - { - if (stream == null) - continue; - - // Trimmed down logic from LegacyScoreDecoder to extract the version from replays. - using (SerializationReader sr = new SerializationReader(stream)) - { - sr.ReadByte(); // Ruleset. - int version = sr.ReadInt32(); - if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) - score.IsLegacyScore = true; - } - } - } - catch (Exception e) - { - Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); - } + sr.ReadByte(); // Ruleset. + int version = sr.ReadInt32(); + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + score.IsLegacyScore = true; + }); } break; @@ -999,52 +980,34 @@ namespace osu.Game.Database if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset()) continue; - string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); - if (replayFilename == null) - continue; - - try + score.PopulateFromReplay(files, sr => { - using (var stream = files.Store.GetStream(replayFilename)) - { - if (stream == null) - continue; + sr.ReadByte(); // Ruleset. + sr.ReadInt32(); // Version. + sr.ReadString(); // Beatmap hash. + sr.ReadString(); // Username. + sr.ReadString(); // MD5Hash. + sr.ReadUInt16(); // Count300. + sr.ReadUInt16(); // Count100. + sr.ReadUInt16(); // Count50. + sr.ReadUInt16(); // CountGeki. + sr.ReadUInt16(); // CountKatu. + sr.ReadUInt16(); // CountMiss. - // Trimmed down logic from LegacyScoreDecoder to extract the mods bitmask. - using (SerializationReader sr = new SerializationReader(stream)) - { - sr.ReadByte(); // Ruleset. - sr.ReadInt32(); // Version. - sr.ReadString(); // Beatmap hash. - sr.ReadString(); // Username. - sr.ReadString(); // MD5Hash. - sr.ReadUInt16(); // Count300. - sr.ReadUInt16(); // Count100. - sr.ReadUInt16(); // Count50. - sr.ReadUInt16(); // CountGeki. - sr.ReadUInt16(); // CountKatu. - sr.ReadUInt16(); // CountMiss. + // we should have this in LegacyTotalScore already, but if we're reading through this anyways... + int totalScore = sr.ReadInt32(); - // we should have this in LegacyTotalScore already, but if we're reading through this anyways... - int totalScore = sr.ReadInt32(); + sr.ReadUInt16(); // Max combo. + sr.ReadBoolean(); // Perfect. - sr.ReadUInt16(); // Max combo. - sr.ReadBoolean(); // Perfect. + var legacyMods = (LegacyMods)sr.ReadInt32(); - var legacyMods = (LegacyMods)sr.ReadInt32(); + if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + return; - if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) - continue; - - score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); - score.LegacyTotalScore = score.TotalScore = totalScore; - } - } - } - catch (Exception e) - { - Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); - } + score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); + score.LegacyTotalScore = score.TotalScore = totalScore; + }); } break; diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index bc3629c25b..b8afdad294 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -5,7 +5,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.IO.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; @@ -287,6 +290,38 @@ namespace osu.Game.Database } } + /// + /// Used to populate the model using data parsed from its corresponding replay file. + /// + /// The score to run population from replay for. + /// A instance to use for fetching replay. + /// + /// Delegate describing the population to execute. + /// The delegate's argument is a instance which permits to read data from the replay stream. + /// + public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action populationFunc) + { + string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); + if (replayFilename == null) + return; + + try + { + using (var stream = files.Store.GetStream(replayFilename)) + { + if (stream == null) + return; + + using (SerializationReader sr = new SerializationReader(stream)) + populationFunc.Invoke(sr); + } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); + } + } + private class FakeHit : HitObject { private readonly Judgement judgement; From 06e5ef88c041c6298e3b2d1e8d64c5f28171d2ac Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jul 2023 02:30:16 +0200 Subject: [PATCH 164/555] legacy export broken --- osu.Game/Beatmaps/BeatmapManager.cs | 53 ++++++++++++++++++++ osu.Game/Beatmaps/BeatmapSetInfo.cs | 13 +++++ osu.Game/Database/ModelManager.cs | 2 + osu.Game/Localisation/EditorStrings.cs | 5 ++ osu.Game/Rulesets/Objects/BezierConverter.cs | 7 +++ osu.Game/Screens/Edit/Editor.cs | 7 +++ 6 files changed, 87 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 305dc01844..7d8c47ea5a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -18,12 +18,15 @@ using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.Extensions; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Utils; @@ -400,6 +403,56 @@ namespace osu.Game.Beatmaps public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); + /// + /// Creates a copy of the and converts all beatmaps to legacy format, then exports it as a legacy package. + /// + /// + /// + public Task ExportLegacy(BeatmapSetInfo beatmap) + { + var copy = beatmap.Clone(); // does the detach from realm + + // convert all beatmaps to legacy format + foreach (var beatmapInfo in copy.Beatmaps) + { + // Convert beatmap + var file = beatmapInfo.File; + + if (file == null) + continue; + + using var oldStream = new LineBufferedReader(ReadFile(file)); + var beatmapContent = new LegacyBeatmapDecoder().Decode(oldStream); + + foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) + controlPoint.Time = Math.Floor(controlPoint.Time); + + foreach (var hitObject in beatmapContent.HitObjects) + { + hitObject.StartTime = Math.Floor(hitObject.StartTime); + + if (hitObject is IHasPath hasPath && BezierConverter.CountSegments(hasPath.Path.ControlPoints) > 1) + { + var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); + hasPath.Path.ControlPoints.Clear(); + hasPath.Path.ControlPoints.AddRange(newControlPoints); + } + } + + using var newStream = new MemoryStream(); + using var sw = new StreamWriter(newStream, Encoding.UTF8, 1024, true); + new LegacyBeatmapEncoder(beatmapContent, null).Encode(sw); + newStream.Seek(0, SeekOrigin.Begin); + + beatmapInfo.MD5Hash = newStream.ComputeMD5Hash(); + beatmapInfo.Hash = newStream.ComputeSHA2Hash(); + + AddFile(copy, newStream, file.Filename); + } + + return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(copy)); + } + private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) { setInfo.Hash = beatmapImporter.ComputeHash(setInfo); diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 59e413d935..923c498df8 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -90,6 +90,19 @@ namespace osu.Game.Beatmaps return ID == other.ID; } + public BeatmapSetInfo Clone() + { + var clone = (BeatmapSetInfo)this.Detach().MemberwiseClone(); + + for (int i = 0; i < clone.Beatmaps.Count; i++) + clone.Beatmaps[i] = clone.Beatmaps[i].Clone(); + + for (int i = 0; i < clone.Files.Count; i++) + clone.Files[i] = new RealmNamedFileUsage(clone.Files[i].File, clone.Files[i].Filename); + + return clone; + } + public override string ToString() => Metadata.GetDisplayString(); public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b); diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 7d1dc5239a..8dafe0874c 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -33,6 +33,8 @@ namespace osu.Game.Database Realm = realm; } + public Stream ReadFile(RealmNamedFileUsage file) => realmFileStore.Storage.GetStream(file.File.GetStoragePath()); + public void DeleteFile(TModel item, RealmNamedFileUsage file) => performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 20258b9c35..f1f650cf66 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ExportPackage => new TranslatableString(getKey(@"export_package"), @"Export package"); + /// + /// "Export legacy package" + /// + public static LocalisableString ExportLegacyPackage => new TranslatableString(getKey(@"export_legacy_package"), @"Export legacy package"); + /// /// "Create new difficulty" /// diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index ebee36a7db..0c878fa1fd 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -39,6 +39,13 @@ namespace osu.Game.Rulesets.Objects new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) }) }; + /// + /// Counts the number of segments in a slider path. + /// + /// The control points of the path. + /// The number of segments in a slider path. + public static int CountSegments(IList controlPoints) => controlPoints.Where((t, i) => t.Type != null && i < controlPoints.Count - 1).Count(); + /// /// Converts a slider path to bezier control point positions compatible with the legacy osu! client. /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b8fa7f6579..bc5df37570 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -966,6 +966,7 @@ namespace osu.Game.Screens.Edit { new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), new EditorMenuItem(EditorStrings.ExportPackage, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(EditorStrings.ExportLegacyPackage, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, new EditorMenuItemSpacer(), createDifficultyCreationMenu(), createDifficultySwitchMenu(), @@ -981,6 +982,12 @@ namespace osu.Game.Screens.Edit beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } + private void exportLegacyBeatmap() + { + Save(); + beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo); + } + /// /// Beatmaps of the currently edited set, grouped by ruleset and ordered by difficulty. /// From 82364b4f9f8ceb5fdc5512fb763067b84cf3696e Mon Sep 17 00:00:00 2001 From: Zyf Date: Tue, 11 Jul 2023 02:46:32 +0200 Subject: [PATCH 165/555] Use OsuScoreProcessor in the scoring test scene --- osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs index 2b378c8013..c722d67ac9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs @@ -17,7 +17,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; @@ -125,8 +125,8 @@ namespace osu.Game.Tests.Visual.Gameplay graphs.Clear(); legend.Clear(); - runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised); - runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic); + runForProcessor("lazer-standardised", Color4.YellowGreen, new OsuScoreProcessor(), ScoringMode.Standardised); + runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), ScoringMode.Classic); runScoreV1(); runScoreV2(); From ca9c31b492dc097b92c172703bfdaa5a4ce8d4a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jul 2023 17:29:28 +0900 Subject: [PATCH 166/555] Add test coverage of slider blueprint end placement failing outside playfield --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 7542e00a94..1d136fe9cc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -61,6 +61,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.Linear); } + [Test] + public void TestPlaceWithMouseMovementOutsidePlayfield() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(1400, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertLength(1200); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + } + [Test] public void TestPlaceNormalControlPoint() { From a0e6748882caf26d24ec526ec882a47968b7505a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jul 2023 17:29:53 +0900 Subject: [PATCH 167/555] Fix slider blueprint placement when ending placement outside the playfield As mentioned in https://github.com/ppy/osu/discussions/24161 --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 717c026ded..5cb9adfd72 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Edit /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) == true; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; protected override bool Handle(UIEvent e) { From 2dcd79044289fe31629965ffcbb6fd11e678790e Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 11 Jul 2023 11:42:31 +0200 Subject: [PATCH 168/555] Resolve `Clipboard` via DI --- osu.Game/Graphics/ScreenshotManager.cs | 5 ++++- osu.Game/Graphics/UserInterface/ExternalLinkButton.cs | 10 ++++++++-- osu.Game/Online/Chat/ExternalLinkOpener.cs | 5 ++++- osu.Game/Overlays/Comments/DrawableComment.cs | 4 ++-- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 4 ++-- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 82f89d6889..26e499ae9a 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -43,6 +43,9 @@ namespace osu.Game.Graphics [Resolved] private GameHost host { get; set; } + [Resolved] + private Clipboard clipboard { get; set; } + private Storage storage; [Resolved] @@ -116,7 +119,7 @@ namespace osu.Game.Graphics using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { - host.GetClipboard()?.SetImage(image); + clipboard.SetImage(image); (string filename, var stream) = getWritableStream(); diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index 4eccb37613..7ba3d55162 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -27,6 +27,9 @@ namespace osu.Game.Graphics.UserInterface [Resolved] private GameHost host { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -92,8 +95,11 @@ namespace osu.Game.Graphics.UserInterface private void copyUrl() { - host.GetClipboard()?.SetText(Link); - onScreenDisplay?.Display(new CopyUrlToast()); + if (Link != null) + { + clipboard.SetText(Link); + onScreenDisplay?.Display(new CopyUrlToast()); + } } } } diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 201212c648..56d24e35bb 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -18,6 +18,9 @@ namespace osu.Game.Online.Chat [Resolved] private GameHost host { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + [Resolved(CanBeNull = true)] private IDialogOverlay? dialogOverlay { get; set; } @@ -32,7 +35,7 @@ namespace osu.Game.Online.Chat public void OpenUrlExternally(string url, bool bypassWarning = false) { if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null) - dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url))); + dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url))); else host.OpenUrlExternally(url); } diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index a710406548..ba1c7ca8b2 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Comments private IAPIProvider api { get; set; } = null!; [Resolved] - private GameHost host { get; set; } = null!; + private Clipboard clipboard { get; set; } = null!; [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -444,7 +444,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - host.GetClipboard()?.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 433fb5c8ee..0a58b34da6 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose public partial class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings { [Resolved] - private GameHost host { get; set; } + private Clipboard hostClipboard { get; set; } = null!; [Resolved] private EditorClock clock { get; set; } @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Edit.Compose // regardless of whether anything was even selected at all. // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. // note that this means that `getTimestamp()` must handle no-selection case, too. - host.GetClipboard()?.SetText(getTimestamp()); + hostClipboard.SetText(getTimestamp()); if (CanCopy.Value) clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); From 1dae1d8f0afa6bb3bae418d5286819bebe4f1612 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Tue, 11 Jul 2023 13:40:19 +0300 Subject: [PATCH 169/555] Account for break time --- osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs index 99a74e6479..21f053f2c2 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckDrainTime.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - double drainTime = context.Beatmap.CalculatePlayableLength(); + double drainTime = context.Beatmap.CalculatePlayableLength() - context.Beatmap.TotalBreakTime; if (drainTime < min_drain_threshold) yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000)); From d75887bb3b54b0e024eb5fe47368b79f169c7733 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Tue, 11 Jul 2023 13:40:27 +0300 Subject: [PATCH 170/555] Apply feedback to tests --- .../Editing/Checks/CheckDrainTimeTest.cs | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index ffa3af8fc6..9f93ec17d5 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -1,6 +1,7 @@ // 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 NUnit.Framework; using osu.Game.Beatmaps; @@ -16,8 +17,6 @@ namespace osu.Game.Tests.Editing.Checks { private CheckDrainTime check = null!; - private IBeatmap beatmap = null!; - [SetUp] public void Setup() { @@ -27,24 +26,77 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestDrainTimeShort() { - setShortDrainTimeBeatmap(); - var content = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + assertShortDrainTime(getShortDrainTimeBeatmap()); + } - var issues = check.Run(content).ToList(); + [Test] + public void TestDrainTimeBreak() + { + assertShortDrainTime(getLongBreakBeatmap()); + } + + [Test] + public void TestDrainTimeCorrect() + { + assertOk(getCorrectDrainTimeBeatmap()); + } + + private IBeatmap getShortDrainTimeBeatmap() + { + return new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 } + } + }; + } + + private IBeatmap getLongBreakBeatmap() + { + return new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 30 } + } + }; + } + + private IBeatmap getCorrectDrainTimeBeatmap() + { + var hitObjects = new List(); + + for (int i = 0; i <= 30; ++i) + { + hitObjects.Add(new HitCircle { StartTime = 1000 * i }); + } + + return new Beatmap + { + HitObjects = hitObjects + }; + } + + private void assertShortDrainTime(IBeatmap beatmap) + { + var issues = check.Run(getContext(beatmap)).ToList(); Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); } - private void setShortDrainTimeBeatmap() + private void assertOk(IBeatmap beatmap) { - beatmap = new Beatmap - { - HitObjects = - { - new HitCircle() - } - }; + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Is.Empty); + } + + private BeatmapVerifierContext getContext(IBeatmap beatmap) + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); } } } From 2db25722cbf29eb7d93e59df5d3e6b414e4d2dd4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jul 2023 20:18:54 +0200 Subject: [PATCH 171/555] It works now --- osu.Game/Beatmaps/BeatmapManager.cs | 89 +++++++++++++++++++---------- osu.Game/Beatmaps/BeatmapSetInfo.cs | 13 ----- osu.Game/Database/ModelManager.cs | 10 ++-- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7d8c47ea5a..7c21b87a97 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,6 +29,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Utils; +using osuTK; namespace osu.Game.Beatmaps { @@ -406,51 +407,77 @@ namespace osu.Game.Beatmaps /// /// Creates a copy of the and converts all beatmaps to legacy format, then exports it as a legacy package. /// - /// + /// /// - public Task ExportLegacy(BeatmapSetInfo beatmap) + public Task ExportLegacy(BeatmapSetInfo beatmapSetInfo) { - var copy = beatmap.Clone(); // does the detach from realm + // Create a clone of the original beatmap set which we will convert to legacy format and then export + var clone = new BeatmapSetInfo(beatmapSetInfo.Beatmaps.Select(b => b.Clone())); + clone.Files.AddRange(beatmapSetInfo.Files.Select(f => new RealmNamedFileUsage(f.File, f.Filename))); - // convert all beatmaps to legacy format - foreach (var beatmapInfo in copy.Beatmaps) + // convert all beatmaps in the cloned beatmap set to legacy format + foreach (var beatmapInfo in clone.Beatmaps) { - // Convert beatmap - var file = beatmapInfo.File; + beatmapInfo.BeatmapSet = clone; + beatmapInfo.ID = Guid.NewGuid(); + var file = beatmapInfo.File; if (file == null) continue; - using var oldStream = new LineBufferedReader(ReadFile(file)); - var beatmapContent = new LegacyBeatmapDecoder().Decode(oldStream); - - foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) - controlPoint.Time = Math.Floor(controlPoint.Time); - - foreach (var hitObject in beatmapContent.HitObjects) + var beatmapContent = new LegacyBeatmapDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))); + var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { - hitObject.StartTime = Math.Floor(hitObject.StartTime); + Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))) + }; - if (hitObject is IHasPath hasPath && BezierConverter.CountSegments(hasPath.Path.ControlPoints) > 1) - { - var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); - hasPath.Path.ControlPoints.Clear(); - hasPath.Path.ControlPoints.AddRange(newControlPoints); - } - } + Realm.Realm.Write(realm => + { + using var stream = new MemoryStream(); + convertAndEncodeLegacyBeatmap(beatmapContent, beatmapSkin, stream); - using var newStream = new MemoryStream(); - using var sw = new StreamWriter(newStream, Encoding.UTF8, 1024, true); - new LegacyBeatmapEncoder(beatmapContent, null).Encode(sw); - newStream.Seek(0, SeekOrigin.Begin); + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + beatmapInfo.Hash = stream.ComputeSHA2Hash(); - beatmapInfo.MD5Hash = newStream.ComputeMD5Hash(); - beatmapInfo.Hash = newStream.ComputeSHA2Hash(); - - AddFile(copy, newStream, file.Filename); + file.File = RealmFileStore.Add(stream, realm).Detach(); + }); } - return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(copy)); + return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); + } + + private void convertAndEncodeLegacyBeatmap(IBeatmap beatmapContent, ISkin beatmapSkin, Stream stream) + { + // Convert beatmap elements to be compatible with legacy format + // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves + foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) + controlPoint.Time = Math.Floor(controlPoint.Time); + + foreach (var hitObject in beatmapContent.HitObjects) + { + hitObject.StartTime = Math.Floor(hitObject.StartTime); + + if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + + var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); + + // Truncate control points to integer positions + foreach (var pathControlPoint in newControlPoints) + { + pathControlPoint.Position = new Vector2( + (float)Math.Floor(pathControlPoint.Position.X), + (float)Math.Floor(pathControlPoint.Position.Y)); + } + + hasPath.Path.ControlPoints.Clear(); + hasPath.Path.ControlPoints.AddRange(newControlPoints); + } + + // Encode to legacy format + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); } private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 923c498df8..59e413d935 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -90,19 +90,6 @@ namespace osu.Game.Beatmaps return ID == other.ID; } - public BeatmapSetInfo Clone() - { - var clone = (BeatmapSetInfo)this.Detach().MemberwiseClone(); - - for (int i = 0; i < clone.Beatmaps.Count; i++) - clone.Beatmaps[i] = clone.Beatmaps[i].Clone(); - - for (int i = 0; i < clone.Files.Count; i++) - clone.Files[i] = new RealmNamedFileUsage(clone.Files[i].File, clone.Files[i].Filename); - - return clone; - } - public override string ToString() => Metadata.GetDisplayString(); public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b); diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 8dafe0874c..172a2df5a3 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -25,16 +25,14 @@ namespace osu.Game.Database protected RealmAccess Realm { get; } - private readonly RealmFileStore realmFileStore; + protected readonly RealmFileStore RealmFileStore; public ModelManager(Storage storage, RealmAccess realm) { - realmFileStore = new RealmFileStore(realm, storage); + RealmFileStore = new RealmFileStore(realm, storage); Realm = realm; } - public Stream ReadFile(RealmNamedFileUsage file) => realmFileStore.Storage.GetStream(file.File.GetStoragePath()); - public void DeleteFile(TModel item, RealmNamedFileUsage file) => performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); @@ -79,7 +77,7 @@ namespace osu.Game.Database /// public void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm) { - file.File = realmFileStore.Add(contents, realm); + file.File = RealmFileStore.Add(contents, realm); } /// @@ -95,7 +93,7 @@ namespace osu.Game.Database return; } - var file = realmFileStore.Add(contents, realm); + var file = RealmFileStore.Add(contents, realm); var namedUsage = new RealmNamedFileUsage(file, filename); item.Files.Add(namedUsage); From b577b6b6ae9ce8e4b047cfed8ba0820c04546db7 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jul 2023 21:04:09 +0200 Subject: [PATCH 172/555] Export legacy converted beatmaps as .osz and non-converted beatmaps as .osz2 --- osu.Game/Beatmaps/BeatmapImporter.cs | 4 ++-- osu.Game/Beatmaps/BeatmapManager.cs | 13 ++++++++++--- osu.Game/Database/BeatmapExporter.cs | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Database/BeatmapExporter.cs diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 7d367ef77d..a2d74d089f 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// public class BeatmapImporter : RealmArchiveModelImporter { - public override IEnumerable HandledExtensions => new[] { ".osz" }; + public override IEnumerable HandledExtensions => new[] { ".osz", ".osz2" }; protected override string[] HashableFileTypes => new[] { ".osu" }; @@ -145,7 +145,7 @@ namespace osu.Game.Beatmaps } } - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz"; + protected override bool ShouldDeleteArchive(string path) => HandledExtensions.Contains(Path.GetExtension(path).ToLowerInvariant()); protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7c21b87a97..ded26f79cd 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -44,7 +44,9 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; - private readonly LegacyBeatmapExporter beatmapExporter; + private readonly BeatmapExporter beatmapExporter; + + private readonly LegacyBeatmapExporter legacyBeatmapExporter; public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; } @@ -81,7 +83,12 @@ namespace osu.Game.Beatmaps workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); - beatmapExporter = new LegacyBeatmapExporter(storage) + beatmapExporter = new BeatmapExporter(storage) + { + PostNotification = obj => PostNotification?.Invoke(obj) + }; + + legacyBeatmapExporter = new LegacyBeatmapExporter(storage) { PostNotification = obj => PostNotification?.Invoke(obj) }; @@ -443,7 +450,7 @@ namespace osu.Game.Beatmaps }); } - return beatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); + return legacyBeatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); } private void convertAndEncodeLegacyBeatmap(IBeatmap beatmapContent, ISkin beatmapSkin, Stream stream) diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs new file mode 100644 index 0000000000..b3a85dd5d9 --- /dev/null +++ b/osu.Game/Database/BeatmapExporter.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; +using osu.Game.Beatmaps; + +namespace osu.Game.Database +{ + public class BeatmapExporter : LegacyArchiveExporter + { + public BeatmapExporter(Storage storage) + : base(storage) + { + } + + protected override string FileExtension => @".osz2"; + } +} From 9a3cb624a8a616b37df9f35280d659c704f6ae18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jul 2023 23:18:49 +0200 Subject: [PATCH 173/555] Rewrite tests to be less aggressively DRY Four of six helper methods defined in the test were used exactly once; the remaining two were used two times. Splitting helpers there is just too much. --- .../Editing/Checks/CheckDrainTimeTest.cs | 69 +++++++------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index 9f93ec17d5..e5a33d753c 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -26,35 +26,25 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestDrainTimeShort() { - assertShortDrainTime(getShortDrainTimeBeatmap()); - } - - [Test] - public void TestDrainTimeBreak() - { - assertShortDrainTime(getLongBreakBeatmap()); - } - - [Test] - public void TestDrainTimeCorrect() - { - assertOk(getCorrectDrainTimeBeatmap()); - } - - private IBeatmap getShortDrainTimeBeatmap() - { - return new Beatmap + var beatmap = new Beatmap { HitObjects = { new HitCircle { StartTime = 0 } } }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); } - private IBeatmap getLongBreakBeatmap() + [Test] + public void TestDrainTimeBreak() { - return new Beatmap + var beatmap = new Beatmap { HitObjects = { @@ -62,41 +52,28 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 30 } } }; - } + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); - private IBeatmap getCorrectDrainTimeBeatmap() - { - var hitObjects = new List(); - - for (int i = 0; i <= 30; ++i) - { - hitObjects.Add(new HitCircle { StartTime = 1000 * i }); - } - - return new Beatmap - { - HitObjects = hitObjects - }; - } - - private void assertShortDrainTime(IBeatmap beatmap) - { - var issues = check.Run(getContext(beatmap)).ToList(); + var issues = check.Run(context).ToList(); Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); } - private void assertOk(IBeatmap beatmap) + [Test] + public void TestDrainTimeCorrect() { - var issues = check.Run(getContext(beatmap)).ToList(); + var hitObjects = new List(); + + for (int i = 0; i <= 30; ++i) + hitObjects.Add(new HitCircle { StartTime = 1000 * i }); + + var beatmap = new Beatmap { HitObjects = hitObjects }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); Assert.That(issues, Is.Empty); } - - private BeatmapVerifierContext getContext(IBeatmap beatmap) - { - return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); - } } } From d927cb3f1c3ec6c61e9ab1ee8beced9ecad5938a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jul 2023 23:21:03 +0200 Subject: [PATCH 174/555] Actually cover cases with breaks in tests --- osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index e5a33d753c..9e20164972 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; @@ -49,7 +50,11 @@ namespace osu.Game.Tests.Editing.Checks HitObjects = { new HitCircle { StartTime = 0 }, - new HitCircle { StartTime = 30 } + new HitCircle { StartTime = 40_000 } + }, + Breaks = new List + { + new BreakPeriod(10_000, 21_000) } }; var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); From a1da0b58db3ebc7ec425df2ff3ae81fc2d9949b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jul 2023 23:22:37 +0200 Subject: [PATCH 175/555] Improve negative test case without breaks too --- osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs index 9e20164972..f845d3c4c1 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainTimeTest.cs @@ -31,7 +31,8 @@ namespace osu.Game.Tests.Editing.Checks { HitObjects = { - new HitCircle { StartTime = 0 } + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 29_999 } } }; var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); From 41d39243264fbd1c1d135cf48d430b757c06b0df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Jul 2023 16:30:26 +0900 Subject: [PATCH 176/555] Update localisation analyser packages --- .config/dotnet-tools.json | 2 +- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 8c8a3be771..3cecb0d07c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2022.809.0", + "version": "2023.712.0", "commands": [ "localisation" ] diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7968364243..8febabb61b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 87570ed238bb303237d40aa3a51a3af46317e17d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 17:23:31 +0900 Subject: [PATCH 177/555] Fix incorrect slider stacking on very old beatmaps Closes https://github.com/ppy/osu/issues/24185 The stable code has had a bug in this logic forever. So we'll need to reimplement the bug. Basically, sliders have to have `UpdateCalculations` run in order to have a correct `Position2` and `EndTime`, but this wasn't being called in the inner loop before use of `EndTime` at https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjectManager.cs#L1813. To fix this, we use `StartTime` in the inner loop to reproduce the bug. --- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index f51f04bf87..c081df3ac6 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -214,17 +214,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps ? currSlider.Position + currSlider.Path.PositionAt(1) : currHitObject.Position; + // Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`. + // This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j) + // and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`. + // + // Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where + // if we use `EndTime` here it would result in unexpected stacking. + if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance) { currHitObject.StackHeight++; - startTime = beatmap.HitObjects[j].GetEndTime(); + startTime = beatmap.HitObjects[j].StartTime; } else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance) { // Case for sliders - bump notes down and right, rather than up and left. sliderStack++; beatmap.HitObjects[j].StackHeight -= sliderStack; - startTime = beatmap.HitObjects[j].GetEndTime(); + startTime = beatmap.HitObjects[j].StartTime; } } } From d12845d7b1ecb007b6d2c9602e1042d192842b12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 17:39:54 +0900 Subject: [PATCH 178/555] Remove no-longer-necessary `ReceivePositionalInputAt` overide in `CatchPlacementBlueprint` --- .../Edit/Blueprints/CatchPlacementBlueprint.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index d2d605a6fe..1a2990e4ac 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osuTK; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { @@ -24,7 +23,5 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints : base(new THitObject()) { } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; } } From 547f2476694ace63aa44d8f55324e61804030f2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 17:41:58 +0900 Subject: [PATCH 179/555] Fix test to work regardless of screen sizes --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 1d136fe9cc..7d29670daa 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -67,11 +67,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(new Vector2(200)); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(1400, 200)); + AddStep("move mouse out of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + Vector2.One)); addClickStep(MouseButton.Right); assertPlaced(true); - assertLength(1200); assertControlPointCount(2); assertControlPointType(0, PathType.Linear); } From b3b6df6e3034051d87bb2dc9ddc3f74bcce5fab2 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 06:19:48 -0400 Subject: [PATCH 180/555] Remove emoji regex --- osu.Game/Online/Chat/MessageFormatter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index f89939d7cf..792780595f 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -283,8 +283,6 @@ namespace osu.Game.Online.Chat while (space-- > 0) empty += "\0"; - handleMatches(emoji_regex, empty, "{0}", result, startIndex); - return result; } From 4f4c481a678b8fb5e608ca9c7af5ce4372ac80f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jul 2023 19:21:24 +0900 Subject: [PATCH 181/555] Fix timing distribution graph sometimes not displaying correctly Weird "basal" height logic just didn't make any sense (was getting stuck at 1 when `DrawHeight` was 0) --- .../HitEventTimingDistributionGraph.cs | 92 ++++++------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 6b1850002d..16da8c64a0 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -211,7 +211,8 @@ namespace osu.Game.Screens.Ranking.Statistics private readonly bool isCentre; private readonly float totalValue; - private float basalHeight; + private const float minimum_height = 0.02f; + private float offsetAdjustment; private Circle[] boxOriginals = null!; @@ -256,15 +257,17 @@ namespace osu.Game.Screens.Ranking.Statistics else { // A bin with no value draws a grey dot instead. - Circle dot = new Circle + InternalChildren = boxOriginals = new[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Colour = isCentre ? Color4.White : Color4.Gray, - Height = 0, + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Colour = isCentre ? Color4.White : Color4.Gray, + Height = 0, + } }; - InternalChildren = boxOriginals = new[] { dot }; } } @@ -272,31 +275,24 @@ namespace osu.Game.Screens.Ranking.Statistics { base.LoadComplete(); - if (!values.Any()) - return; - - updateBasalHeight(); - - foreach (var boxOriginal in boxOriginals) - { - boxOriginal.Y = 0; - boxOriginal.Height = basalHeight; - } - float offsetValue = 0; - for (int i = 0; i < values.Count; i++) + for (int i = 0; i < boxOriginals.Length; i++) { - boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); - boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint); - offsetValue -= values[i].Value; - } - } + int value = i < values.Count ? values[i].Value : 0; - protected override void Update() - { - base.Update(); - updateBasalHeight(); + var box = boxOriginals[i]; + + box.Y = 0; + box.Height = 0; + + box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); + box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); + offsetValue -= value; + } + + if (boxAdjustment != null) + drawAdjustmentBar(); } public void UpdateOffset(float adjustment) @@ -324,43 +320,9 @@ namespace osu.Game.Screens.Ranking.Statistics drawAdjustmentBar(); } - private void updateBasalHeight() - { - float newBasalHeight = DrawHeight > DrawWidth ? DrawWidth / DrawHeight : 1; + private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; - if (newBasalHeight == basalHeight) - return; - - basalHeight = newBasalHeight; - foreach (var dot in boxOriginals) - dot.Height = basalHeight; - - draw(); - } - - private float offsetForValue(float value) => (1 - basalHeight) * value / maxValue; - - private float heightForValue(float value) => MathF.Max(basalHeight + offsetForValue(value), 0); - - private void draw() - { - resizeBars(); - - if (boxAdjustment != null) - drawAdjustmentBar(); - } - - private void resizeBars() - { - float offsetValue = 0; - - for (int i = 0; i < values.Count; i++) - { - boxOriginals[i].Y = offsetForValue(offsetValue) * DrawHeight; - boxOriginals[i].Height = heightForValue(values[i].Value); - offsetValue -= values[i].Value; - } - } + private float heightForValue(float value) => minimum_height + offsetForValue(value); private void drawAdjustmentBar() { From b05ba8c501cd1d43b19bbe057ca939fdca6e4f06 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 06:32:33 -0400 Subject: [PATCH 182/555] Remove unused code --- osu.Game/Online/Chat/MessageFormatter.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 792780595f..6ca651bc87 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -279,10 +279,6 @@ namespace osu.Game.Online.Chat // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); - string empty = ""; - while (space-- > 0) - empty += "\0"; - return result; } From 465cc759f02c84c6c3ae720d715623ac2b7596d6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 12 Jul 2023 14:49:49 +0200 Subject: [PATCH 183/555] Add xmldoc to clarify the purpose of BeatmapExporter --- osu.Game/Database/BeatmapExporter.cs | 4 ++++ osu.Game/Database/LegacyBeatmapExporter.cs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs index b3a85dd5d9..9377065973 100644 --- a/osu.Game/Database/BeatmapExporter.cs +++ b/osu.Game/Database/BeatmapExporter.cs @@ -6,6 +6,10 @@ using osu.Game.Beatmaps; namespace osu.Game.Database { + /// + /// Exporter for beatmap archives. + /// This is not for legacy purposes and works for lazer only. + /// public class BeatmapExporter : LegacyArchiveExporter { public BeatmapExporter(Storage storage) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 4ee8c0636e..62096f862a 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -6,6 +6,9 @@ using osu.Game.Beatmaps; namespace osu.Game.Database { + /// + /// Exporter for osu!stable legacy beatmap archives. + /// public class LegacyBeatmapExporter : LegacyArchiveExporter { public LegacyBeatmapExporter(Storage storage) From 3052c317e10c801e79a45542bcc247ee59379ce5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 12 Jul 2023 15:04:06 +0200 Subject: [PATCH 184/555] change .osz2 to .olz (osu lazer zip) --- osu.Game/Beatmaps/BeatmapImporter.cs | 2 +- osu.Game/Database/BeatmapExporter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index a2d74d089f..e500f87984 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// public class BeatmapImporter : RealmArchiveModelImporter { - public override IEnumerable HandledExtensions => new[] { ".osz", ".osz2" }; + public override IEnumerable HandledExtensions => new[] { ".osz", ".olz" }; protected override string[] HashableFileTypes => new[] { ".osu" }; diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs index 9377065973..f37c57dea5 100644 --- a/osu.Game/Database/BeatmapExporter.cs +++ b/osu.Game/Database/BeatmapExporter.cs @@ -17,6 +17,6 @@ namespace osu.Game.Database { } - protected override string FileExtension => @".osz2"; + protected override string FileExtension => @".olz"; } } From 8ca801a2240b6749c35dc98a33f73ba229d080fe Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 12 Jul 2023 15:18:16 +0200 Subject: [PATCH 185/555] dispose the streams --- osu.Game/Beatmaps/BeatmapManager.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index ded26f79cd..248e97d6ee 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -432,10 +432,15 @@ namespace osu.Game.Beatmaps if (file == null) continue; - var beatmapContent = new LegacyBeatmapDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))); + using var contentStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); + using var contentStreamReader = new LineBufferedReader(contentStream); + var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); + + using var skinStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); + using var skinStreamReader = new LineBufferedReader(contentStream); var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { - Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(RealmFileStore.Storage.GetStream(file.File.GetStoragePath()))) + Configuration = new LegacySkinDecoder().Decode(skinStreamReader) }; Realm.Realm.Write(realm => From 1d837a8725308a7f6e83ced3713a8796ad464930 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 13 Jul 2023 00:20:01 +0200 Subject: [PATCH 186/555] Move all conversion code to LegacyBeatmapExporter --- osu.Game/Beatmaps/BeatmapManager.cs | 85 +--------------------- osu.Game/Database/LegacyArchiveExporter.cs | 4 +- osu.Game/Database/LegacyBeatmapExporter.cs | 70 ++++++++++++++++++ osu.Game/Database/ModelManager.cs | 8 +- 4 files changed, 78 insertions(+), 89 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 248e97d6ee..ffdff13845 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -18,18 +18,14 @@ using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Utils; -using osuTK; namespace osu.Game.Beatmaps { @@ -411,86 +407,7 @@ namespace osu.Game.Beatmaps public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); - /// - /// Creates a copy of the and converts all beatmaps to legacy format, then exports it as a legacy package. - /// - /// - /// - public Task ExportLegacy(BeatmapSetInfo beatmapSetInfo) - { - // Create a clone of the original beatmap set which we will convert to legacy format and then export - var clone = new BeatmapSetInfo(beatmapSetInfo.Beatmaps.Select(b => b.Clone())); - clone.Files.AddRange(beatmapSetInfo.Files.Select(f => new RealmNamedFileUsage(f.File, f.Filename))); - - // convert all beatmaps in the cloned beatmap set to legacy format - foreach (var beatmapInfo in clone.Beatmaps) - { - beatmapInfo.BeatmapSet = clone; - beatmapInfo.ID = Guid.NewGuid(); - - var file = beatmapInfo.File; - if (file == null) - continue; - - using var contentStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); - using var contentStreamReader = new LineBufferedReader(contentStream); - var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); - - using var skinStream = RealmFileStore.Storage.GetStream(file.File.GetStoragePath()); - using var skinStreamReader = new LineBufferedReader(contentStream); - var beatmapSkin = new LegacySkin(new SkinInfo(), null!) - { - Configuration = new LegacySkinDecoder().Decode(skinStreamReader) - }; - - Realm.Realm.Write(realm => - { - using var stream = new MemoryStream(); - convertAndEncodeLegacyBeatmap(beatmapContent, beatmapSkin, stream); - - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - beatmapInfo.Hash = stream.ComputeSHA2Hash(); - - file.File = RealmFileStore.Add(stream, realm).Detach(); - }); - } - - return legacyBeatmapExporter.ExportAsync(new RealmLiveUnmanaged(clone)); - } - - private void convertAndEncodeLegacyBeatmap(IBeatmap beatmapContent, ISkin beatmapSkin, Stream stream) - { - // Convert beatmap elements to be compatible with legacy format - // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves - foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) - controlPoint.Time = Math.Floor(controlPoint.Time); - - foreach (var hitObject in beatmapContent.HitObjects) - { - hitObject.StartTime = Math.Floor(hitObject.StartTime); - - if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; - - var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); - - // Truncate control points to integer positions - foreach (var pathControlPoint in newControlPoints) - { - pathControlPoint.Position = new Vector2( - (float)Math.Floor(pathControlPoint.Position.X), - (float)Math.Floor(pathControlPoint.Position.Y)); - } - - hasPath.Path.ControlPoints.Clear(); - hasPath.Path.ControlPoints.AddRange(newControlPoints); - } - - // Encode to legacy format - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - } + public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm)); private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) { diff --git a/osu.Game/Database/LegacyArchiveExporter.cs b/osu.Game/Database/LegacyArchiveExporter.cs index 7689ffc13d..9805207591 100644 --- a/osu.Game/Database/LegacyArchiveExporter.cs +++ b/osu.Game/Database/LegacyArchiveExporter.cs @@ -39,7 +39,7 @@ namespace osu.Game.Database { cancellationToken.ThrowIfCancellationRequested(); - using (var stream = UserFileStorage.GetStream(file.File.GetStoragePath())) + using (var stream = GetFileContents(model, file)) { if (stream == null) { @@ -65,5 +65,7 @@ namespace osu.Game.Database } } } + + protected virtual Stream? GetFileContents(TModel model, INamedFileUsage file) => UserFileStorage.GetStream(file.File.GetStoragePath()); } } diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 62096f862a..42d8a72073 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -1,13 +1,25 @@ // 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.IO; +using System.Linq; +using System.Text; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.Extensions; +using osu.Game.IO; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Database { /// /// Exporter for osu!stable legacy beatmap archives. + /// Converts all beatmaps in the set to legacy format and exports it as a legacy package. /// public class LegacyBeatmapExporter : LegacyArchiveExporter { @@ -16,6 +28,64 @@ namespace osu.Game.Database { } + protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file) + { + bool isBeatmap = model.Beatmaps.Any(o => o.Hash == file.File.Hash); + + if (!isBeatmap) + return base.GetFileContents(model, file); + + // Read the beatmap contents and skin + using var contentStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + + if (contentStream == null) + return null; + + using var contentStreamReader = new LineBufferedReader(contentStream); + var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); + + using var skinStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + using var skinStreamReader = new LineBufferedReader(contentStream); + var beatmapSkin = new LegacySkin(new SkinInfo(), null!) + { + Configuration = new LegacySkinDecoder().Decode(skinStreamReader) + }; + + // Convert beatmap elements to be compatible with legacy format + // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves + foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) + controlPoint.Time = Math.Floor(controlPoint.Time); + + foreach (var hitObject in beatmapContent.HitObjects) + { + hitObject.StartTime = Math.Floor(hitObject.StartTime); + + if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + + var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); + + // Truncate control points to integer positions + foreach (var pathControlPoint in newControlPoints) + { + pathControlPoint.Position = new Vector2( + (float)Math.Floor(pathControlPoint.Position.X), + (float)Math.Floor(pathControlPoint.Position.Y)); + } + + hasPath.Path.ControlPoints.Clear(); + hasPath.Path.ControlPoints.AddRange(newControlPoints); + } + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + protected override string FileExtension => @".osz"; } } diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 172a2df5a3..7d1dc5239a 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -25,11 +25,11 @@ namespace osu.Game.Database protected RealmAccess Realm { get; } - protected readonly RealmFileStore RealmFileStore; + private readonly RealmFileStore realmFileStore; public ModelManager(Storage storage, RealmAccess realm) { - RealmFileStore = new RealmFileStore(realm, storage); + realmFileStore = new RealmFileStore(realm, storage); Realm = realm; } @@ -77,7 +77,7 @@ namespace osu.Game.Database /// public void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm) { - file.File = RealmFileStore.Add(contents, realm); + file.File = realmFileStore.Add(contents, realm); } /// @@ -93,7 +93,7 @@ namespace osu.Game.Database return; } - var file = RealmFileStore.Add(contents, realm); + var file = realmFileStore.Add(contents, realm); var namedUsage = new RealmNamedFileUsage(file, filename); item.Files.Add(namedUsage); From d62cfc16166ff48f07778a67d37fe939e71e80c0 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 20:24:09 -0400 Subject: [PATCH 187/555] Parse emoji to an empty string --- osu.Game/Online/Chat/MessageFormatter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 6ca651bc87..3e03cc287b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -256,6 +256,9 @@ namespace osu.Game.Online.Chat private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { + // see: https://github.com/ppy/osu/pull/24190 + toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), string.Empty); + var result = new MessageFormatterResult(toFormat); // handle the [link display] format From 5a43de1ace9e81e8e838c077f4fc79d5dbb1d229 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 21:13:10 -0400 Subject: [PATCH 188/555] Update test cases --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 3c35dc311f..fef8054c70 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -478,7 +478,7 @@ namespace osu.Game.Tests.Chat Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12" }); - Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent); + Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!", result.DisplayContent); Assert.AreEqual(5, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links"); @@ -508,6 +508,7 @@ namespace osu.Game.Tests.Chat } [Test] + [Ignore("https://github.com/ppy/osu/pull/24190")] public void TestEmoji() { Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); From 3d256acfef02706afcb51316161314dd46edd19d Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 21:40:49 -0400 Subject: [PATCH 189/555] Delete emoji test in TestLinkComplex --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index fef8054c70..5c063a5c6c 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -500,11 +500,6 @@ namespace osu.Game.Tests.Chat Assert.That(f, Is.Not.Null); Assert.AreEqual(78, f.Index); Assert.AreEqual(18, f.Length); - - f = result.Links.Find(l => l.Url == "\uD83D\uDE12"); - Assert.That(f, Is.Not.Null); - Assert.AreEqual(101, f.Index); - Assert.AreEqual(3, f.Length); } [Test] From f44e6e510d967792e55b86b7163a9d70addcf499 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 12 Jul 2023 22:42:12 -0400 Subject: [PATCH 190/555] 5 -> 4 --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 5c063a5c6c..aa45e360e6 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -479,7 +479,7 @@ namespace osu.Game.Tests.Chat }); Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!", result.DisplayContent); - Assert.AreEqual(5, result.Links.Count); + Assert.AreEqual(4, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links"); Assert.That(f, Is.Not.Null); From 8e294c325842430f56189ef8142a4b288279c07f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:22:45 +0900 Subject: [PATCH 191/555] Add test coverage of hitting objects immediately after a swell --- .../Judgements/TestSceneSwellJudgements.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs index ccc829f09e..4abad98eab 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs @@ -114,5 +114,75 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); } + + /// + /// Ensure input is correctly sent to subsequent hits if a swell is fully completed. + /// + [Test] + public void TestHitSwellThenHitHit() + { + const double swell_time = 1000; + const double hit_time = 1150; + + Swell swell = new Swell + { + StartTime = swell_time, + Duration = 100, + RequiredHits = 1 + }; + + Hit hit = new Hit + { + StartTime = hit_time + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(swell_time, TaikoAction.LeftRim), + new TaikoReplayFrame(hit_time, TaikoAction.RightCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertJudgementCount(3); + + AssertResult(0, HitResult.IgnoreHit); + AssertResult(0, HitResult.LargeBonus); + AssertResult(0, HitResult.Great); + } + + [Test] + public void TestMissSwellThenHitHit() + { + const double swell_time = 1000; + const double hit_time = 1150; + + Swell swell = new Swell + { + StartTime = swell_time, + Duration = 100, + RequiredHits = 1 + }; + + Hit hit = new Hit + { + StartTime = hit_time + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time, TaikoAction.RightCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertJudgementCount(3); + + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.Great); + } } } From 259ac6d427b3142a99c0366024adb068b97cb47e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:16:58 +0900 Subject: [PATCH 192/555] Fix osu!taiko swells eating input after already being judged --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 8441e3a749..3fa6f4b756 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -276,6 +276,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (Time.Current < HitObject.StartTime) return false; + if (AllJudged) + return false; + bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre; // Ensure alternating centre and rim hits From fbf14a0f7cbd4c9b354c71affb49d8e0406c8584 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:41:35 +0900 Subject: [PATCH 193/555] Allow autoplay to fail Feels more correct. --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 4 +--- osu.Game/Screens/Play/ReplayPlayer.cs | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 83afda3a28..ab2c84bada 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -11,7 +11,7 @@ using osu.Game.Replays; namespace osu.Game.Rulesets.Mods { - public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData + public abstract class ModAutoplay : Mod, ICreateReplayData { public override string Name => "Autoplay"; public override string Acronym => "AT"; @@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; - public bool PerformFail() => false; - public bool RestartOnFail => false; public override bool UserPlayable => false; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 8a4e63d21c..ca71a89b48 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Framework.Input.Bindings; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.Play // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() { - if (!replayIsFailedScore) + if (!replayIsFailedScore && !GameplayState.Mods.OfType().Any()) return false; return base.CheckModsAllowFailure(); From 1bfe5a18cb94de911bb6ecf445abc8d32fe254ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 13:46:50 +0900 Subject: [PATCH 194/555] Rename `RestoreDefaultValueButton` to `RevertToDefaultButton` Because I can't find it every time I search. --- .../Settings/TestSceneKeyBindingPanel.cs | 10 +++--- ...n.cs => TestSceneRevertToDefaultButton.cs} | 10 +++--- .../Visual/Settings/TestSceneSettingsItem.cs | 34 +++++++++---------- .../TestSceneModSelectOverlay.cs | 2 +- ...alueButton.cs => RevertToDefaultButton.cs} | 4 +-- .../Settings/Sections/Input/KeyBindingRow.cs | 2 +- osu.Game/Overlays/Settings/SettingsItem.cs | 2 +- 7 files changed, 32 insertions(+), 32 deletions(-) rename osu.Game.Tests/Visual/Settings/{TestSceneRestoreDefaultValueButton.cs => TestSceneRevertToDefaultButton.cs} (82%) rename osu.Game/Overlays/{RestoreDefaultValueButton.cs => RevertToDefaultButton.cs} (97%) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index da48086717..449ca0f258 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -195,16 +195,16 @@ namespace osu.Game.Tests.Visual.Settings InputManager.ReleaseKey(Key.P); }); - AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); AddStep("click reset button for bindings", () => { - var resetButton = settingsKeyBindingRow.ChildrenOfType>().First(); + var resetButton = settingsKeyBindingRow.ChildrenOfType>().First(); resetButton.TriggerClick(); }); - AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); @@ -225,7 +225,7 @@ namespace osu.Game.Tests.Visual.Settings InputManager.ReleaseKey(Key.P); }); - AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); AddStep("click reset button for bindings", () => { @@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Settings resetButton.TriggerClick(); }); - AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs similarity index 82% rename from osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs rename to osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index 6e52881f5e..609283edfc 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Settings { - public partial class TestSceneRestoreDefaultValueButton : OsuTestScene + public partial class TestSceneRevertToDefaultButton : OsuTestScene { [Resolved] private OsuColour colours { get; set; } @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestBasic() { - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create button", () => Child = new Container { @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Settings RelativeSizeAxes = Axes.Both, Colour = colours.GreySeaFoam }, - restoreDefaultValueButton = new RestoreDefaultValueButton + revertToDefaultButton = new RevertToDefaultButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -55,8 +55,8 @@ namespace osu.Game.Tests.Visual.Settings AddSliderStep("set scale", 1, 4, 1, scale => { this.scale = scale; - if (restoreDefaultValueButton != null) - restoreDefaultValueButton.Scale = new Vector2(scale); + if (revertToDefaultButton != null) + revertToDefaultButton.Scale = new Vector2(scale); }); AddToggleStep("toggle default state", state => current.Value = state ? default : 1); AddToggleStep("toggle disabled state", state => current.Disabled = state); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index 384508f375..ec0ad685c5 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Settings public void TestRestoreDefaultValueButtonVisibility() { SettingsTextBox textBox = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create settings item", () => { @@ -33,22 +33,22 @@ namespace osu.Game.Tests.Visual.Settings }; }); AddUntilStep("wait for loaded", () => textBox.IsLoaded); - AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType>().Single()); + AddStep("retrieve restore default button", () => revertToDefaultButton = textBox.ChildrenOfType>().Single()); - AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0); AddStep("change value from default", () => textBox.Current.Value = "non-default"); - AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0); + AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); AddStep("restore default", () => textBox.Current.SetDefault()); - AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); } [Test] public void TestSetAndClearLabelText() { SettingsTextBox textBox = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; OsuTextBox control = null; AddStep("create settings item", () => @@ -61,25 +61,25 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("wait for loaded", () => textBox.IsLoaded); AddStep("retrieve components", () => { - restoreDefaultValueButton = textBox.ChildrenOfType>().Single(); + revertToDefaultButton = textBox.ChildrenOfType>().Single(); control = textBox.ChildrenOfType().Single(); }); - AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default"); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddStep("set non-default value", () => revertToDefaultButton.Current.Value = "non-default"); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1)); AddStep("set label", () => textBox.LabelText = "label text"); AddAssert("default value button centre aligned to label size", () => { var label = textBox.ChildrenOfType().Single(spriteText => spriteText.Text == "label text"); - return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1); + return Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, label.DrawHeight, 1); }); AddStep("clear label", () => textBox.LabelText = default); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1)); AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true)); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1)); } /// @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Settings { BindableFloat current = null; SettingsSlider sliderBar = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create settings item", () => { @@ -107,15 +107,15 @@ namespace osu.Game.Tests.Visual.Settings }; }); AddUntilStep("wait for loaded", () => sliderBar.IsLoaded); - AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single()); + AddStep("retrieve restore default button", () => revertToDefaultButton = sliderBar.ChildrenOfType>().Single()); - AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0); AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f); - AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0); + AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); AddStep("restore default", () => sliderBar.Current.SetDefault()); - AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 4cb6899ebc..ad79865ad9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -793,7 +793,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() - .ChildrenOfType>().Single().TriggerClick()); + .ChildrenOfType>().Single().TriggerClick()); AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7)); } diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs similarity index 97% rename from osu.Game/Overlays/RestoreDefaultValueButton.cs rename to osu.Game/Overlays/RevertToDefaultButton.cs index 97c66fdf02..fcd8b74009 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -22,7 +22,7 @@ using osu.Game.Localisation; namespace osu.Game.Overlays { - public partial class RestoreDefaultValueButton : OsuClickableContainer, IHasCurrentValue + public partial class RevertToDefaultButton : OsuClickableContainer, IHasCurrentValue { public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; @@ -58,7 +58,7 @@ namespace osu.Game.Overlays private CircularContainer circle = null!; private Box background = null!; - public RestoreDefaultValueButton() + public RevertToDefaultButton() : base(HoverSampleSet.Button) { } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 725925c8cf..1e2283b58b 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { RelativeSizeAxes = Axes.Y, Width = SettingsPanel.CONTENT_MARGINS, - Child = new RestoreDefaultValueButton + Child = new RevertToDefaultButton { Current = isDefault, Action = RestoreDefaults, diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 5f4bb9d57f..9085b6c911 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings // intentionally done before LoadComplete to avoid overhead. if (ShowsDefaultIndicator) { - defaultValueIndicatorContainer.Add(new RestoreDefaultValueButton + defaultValueIndicatorContainer.Add(new RevertToDefaultButton { Current = controlWithCurrent.Current, Anchor = Anchor.Centre, From 94201579f6d6307308cb9823a308d9bd9c6db5cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 14:26:01 +0900 Subject: [PATCH 195/555] Update design of "revert to default" button I keep getting feedback that the old design looked like anything *but* a button to revert defaults. Including people clicking it expecting opposite behaviour. This is intended to be a temporary design until we get the full new UI components online (where this is moved to the right-hand-side). --- .../TestSceneRevertToDefaultButton.cs | 13 ++- osu.Game/Overlays/RevertToDefaultButton.cs | 85 +++++++++---------- 2 files changed, 44 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index 609283edfc..bfef120358 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Overlays; using osuTK; @@ -17,11 +14,11 @@ namespace osu.Game.Tests.Visual.Settings { public partial class TestSceneRevertToDefaultButton : OsuTestScene { - [Resolved] - private OsuColour colours { get; set; } - private float scale = 1; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly Bindable current = new Bindable { Default = default, @@ -31,7 +28,7 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestBasic() { - RevertToDefaultButton revertToDefaultButton = null; + RevertToDefaultButton revertToDefaultButton = null!; AddStep("create button", () => Child = new Container { @@ -41,7 +38,7 @@ namespace osu.Game.Tests.Visual.Settings new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoam + Colour = colourProvider.Background2, }, revertToDefaultButton = new RevertToDefaultButton { diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index fcd8b74009..48491c5d9c 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -1,24 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osuTK; using osu.Game.Localisation; +using osuTK; namespace osu.Game.Overlays { @@ -31,11 +27,17 @@ namespace osu.Game.Overlays // this is intentionally not using BindableWithCurrent, as it can use the wrong IsDefault implementation when passed a BindableNumber. // using GetBoundCopy() ensures that the received bindable is of the exact same type as the source bindable and uses the proper IsDefault implementation. - private Bindable current; + private Bindable? current; + + private SpriteIcon icon = null!; + private Circle circle = null!; + + [Resolved] + private OverlayColourProvider colours { get; set; } = null!; public Bindable Current { - get => current; + get => current.AsNonNull(); set { current?.UnbindAll(); @@ -50,43 +52,37 @@ namespace osu.Game.Overlays } } - [Resolved] - private OsuColour colours { get; set; } - - private const float size = 4; - - private CircularContainer circle = null!; - private Box background = null!; - public RevertToDefaultButton() : base(HoverSampleSet.Button) { } [BackgroundDependencyLoader] - private void load(OsuColour colour) + private void load() { // size intentionally much larger than actual drawn content, so that the button is easier to click. - Size = new Vector2(3 * size); + Size = new Vector2(14); - Add(circle = new CircularContainer + AddRange(new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(size), - Masking = true, - Child = background = new Box + circle = new Circle { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Colour = colour.Lime1 + }, + icon = new SpriteIcon + { + Icon = FontAwesome.Solid.Undo, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(8), } }); - Alpha = 0f; - Action += () => { - if (!current.Disabled) + if (current?.Disabled == false) current.SetDefault(); }; } @@ -120,28 +116,25 @@ namespace osu.Game.Overlays if (current == null) return; - Enabled.Value = !Current.Disabled; + Enabled.Value = !current.Disabled; - if (!Current.Disabled) + this.FadeTo(current.Disabled ? 0.2f : (Current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); + + if (IsHovered && Enabled.Value) { - this.FadeTo(Current.IsDefault ? 0 : 1, fade_duration, Easing.OutQuint); - background.FadeColour(IsHovered ? colours.Lime0 : colours.Lime1, fade_duration, Easing.OutQuint); - circle.TweenEdgeEffectTo(new EdgeEffectParameters - { - Colour = (IsHovered ? colours.Lime1 : colours.Lime3).Opacity(0.4f), - Radius = IsHovered ? 8 : 4, - Type = EdgeEffectType.Glow - }, fade_duration, Easing.OutQuint); + icon.RotateTo(-40, 500, Easing.OutQuint); + + icon.FadeColour(colours.Light1, 300, Easing.OutQuint); + circle.FadeColour(colours.Background2, 300, Easing.OutQuint); + this.ScaleTo(1.2f, 300, Easing.OutQuint); } else { - background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint); - circle.TweenEdgeEffectTo(new EdgeEffectParameters - { - Colour = colours.Lime3.Opacity(0.1f), - Radius = 2, - Type = EdgeEffectType.Glow - }, fade_duration, Easing.OutQuint); + icon.RotateTo(0, 100, Easing.OutQuint); + + icon.FadeColour(colours.Colour0, 100, Easing.OutQuint); + circle.FadeColour(colours.Background3, 100, Easing.OutQuint); + this.ScaleTo(1f, 100, Easing.OutQuint); } } } From e2b5abd4e8e90a0cd6ef169f2fefaad7909a9376 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 14:43:14 +0900 Subject: [PATCH 196/555] Split bar drawable creation into own method --- .../HitEventTimingDistributionGraph.cs | 137 +++++++++--------- 1 file changed, 69 insertions(+), 68 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 16da8c64a0..7e7c0ccb54 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -113,94 +113,95 @@ namespace osu.Game.Screens.Ranking.Statistics } } - if (barDrawables != null) - { - for (int i = 0; i < barDrawables.Length; i++) - { - barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value)); - } - } + if (barDrawables == null) + createBarDrawables(); else { - int maxCount = bins.Max(b => b.Values.Sum()); - barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray(); + for (int i = 0; i < barDrawables.Length; i++) + barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value)); + } + } - Container axisFlow; + private void createBarDrawables() + { + int maxCount = bins.Max(b => b.Values.Sum()); + barDrawables = bins.Select((_, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray(); - const float axis_font_size = 12; + Container axisFlow; - InternalChild = new GridContainer + const float axis_font_size = 12; + + InternalChild = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Width = 0.8f, + Content = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = 0.8f, - Content = new[] + new Drawable[] { - new Drawable[] + new GridContainer { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] { barDrawables } - } - }, - new Drawable[] - { - axisFlow = new Container - { - RelativeSizeAxes = Axes.X, - Height = axis_font_size, - } - }, + RelativeSizeAxes = Axes.Both, + Content = new[] { barDrawables } + } }, - RowDimensions = new[] + new Drawable[] { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } - }; + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + Height = axis_font_size, + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }; - // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - double maxValue = timing_distribution_bins * binSize; - double axisValueStep = maxValue / axis_points; + // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + double maxValue = timing_distribution_bins * binSize; + double axisValueStep = maxValue / axis_points; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "0", + Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + }); + + for (int i = 1; i <= axis_points; i++) + { + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; axisFlow.Add(new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "0", + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) }); - for (int i = 1; i <= axis_points; i++) + axisFlow.Add(new OsuSpriteText { - double axisValue = i * axisValueStep; - float position = (float)(axisValue / maxValue); - float alpha = 1f - position * 0.8f; - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = -position / 2, - Alpha = alpha, - Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) - }); - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = position / 2, - Alpha = alpha, - Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) - }); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + }); } } From 21f26f98da76ee5d89dc7c5c8e32309bd2caf034 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 14:57:01 +0900 Subject: [PATCH 197/555] Fix graph breaking when resized vertically --- .../HitEventTimingDistributionGraph.cs | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 7e7c0ccb54..85c2777645 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; @@ -220,6 +222,8 @@ namespace osu.Game.Screens.Ranking.Statistics private Circle? boxAdjustment; + private float? lastDrawHeight; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -276,24 +280,18 @@ namespace osu.Game.Screens.Ranking.Statistics { base.LoadComplete(); - float offsetValue = 0; + Scheduler.AddOnce(updateMetrics, true); + } - for (int i = 0; i < boxOriginals.Length; i++) + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if (invalidation.HasFlagFast(Invalidation.DrawSize)) { - int value = i < values.Count ? values[i].Value : 0; - - var box = boxOriginals[i]; - - box.Y = 0; - box.Height = 0; - - box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); - box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); - offsetValue -= value; + if (lastDrawHeight != null && lastDrawHeight != DrawHeight) + Scheduler.AddOnce(updateMetrics, false); } - if (boxAdjustment != null) - drawAdjustmentBar(); + return base.OnInvalidate(invalidation, source); } public void UpdateOffset(float adjustment) @@ -318,12 +316,36 @@ namespace osu.Game.Screens.Ranking.Statistics } offsetAdjustment = adjustment; - drawAdjustmentBar(); + + Scheduler.AddOnce(updateMetrics, true); } - private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; + private void updateMetrics(bool animate = true) + { + float offsetValue = 0; - private float heightForValue(float value) => minimum_height + offsetForValue(value); + for (int i = 0; i < boxOriginals.Length; i++) + { + int value = i < values.Count ? values[i].Value : 0; + + var box = boxOriginals[i]; + + box.Y = 0; + box.Height = 0; + + box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); + box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); + offsetValue -= value; + } + + if (boxAdjustment != null) + drawAdjustmentBar(); + + if (!animate) + FinishTransforms(true); + + lastDrawHeight = DrawHeight; + } private void drawAdjustmentBar() { @@ -332,6 +354,10 @@ namespace osu.Game.Screens.Ranking.Statistics boxAdjustment.ResizeHeightTo(heightForValue(offsetAdjustment), duration, Easing.OutQuint); boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint); } + + private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; + + private float heightForValue(float value) => minimum_height + offsetForValue(value); } } } From 1d62a041ccdd8ae10066823080b76861d42ff15d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 15:07:26 +0900 Subject: [PATCH 198/555] Fix animation restarting unexpectedly --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 85c2777645..e8d2eefd81 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -330,9 +330,6 @@ namespace osu.Game.Screens.Ranking.Statistics var box = boxOriginals[i]; - box.Y = 0; - box.Height = 0; - box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); offsetValue -= value; From 3d17a03dc696c340ca690716a380f83a500e3013 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 01:09:35 -0400 Subject: [PATCH 199/555] Emojis now represented as "[emoji]" --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 7 +++---- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index aa45e360e6..529b28318a 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -478,7 +478,7 @@ namespace osu.Game.Tests.Chat Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12" }); - Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!", result.DisplayContent); + Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now![emoji]", result.DisplayContent); Assert.AreEqual(4, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links"); @@ -503,11 +503,10 @@ namespace osu.Game.Tests.Chat } [Test] - [Ignore("https://github.com/ppy/osu/pull/24190")] public void TestEmoji() { - Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); - Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent); + Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); + Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent); Assert.AreEqual(result.Links.Count, 4); Assert.AreEqual(result.Links[0].Index, 11); Assert.AreEqual(result.Links[1].Index, 49); diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 3e03cc287b..7a3941038e 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -257,7 +257,7 @@ namespace osu.Game.Online.Chat private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { // see: https://github.com/ppy/osu/pull/24190 - toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), string.Empty); + toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), "[emoji]"); var result = new MessageFormatterResult(toFormat); From 2f40989a4fb67af56c9e7685794bb5ff42aad2f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 17:25:08 +0900 Subject: [PATCH 200/555] Allow no fail mod during autoplay --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModNoFail.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index ab2c84bada..a4ffbeacef 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; - public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAdaptiveSpeed) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModAdaptiveSpeed) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 31bb4338b3..8c61d948a4 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "You can't fail, no matter what."; public override double ScoreMultiplier => 0.5; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition) }; } } From 403a7aa0006d7103100b02344665918526462df9 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 04:28:35 -0400 Subject: [PATCH 201/555] 0 links are expected --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 529b28318a..4d18a3d0bb 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -507,7 +507,7 @@ namespace osu.Game.Tests.Chat { Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent); - Assert.AreEqual(result.Links.Count, 4); + Assert.AreEqual(result.Links.Count, 0); Assert.AreEqual(result.Links[0].Index, 11); Assert.AreEqual(result.Links[1].Index, 49); Assert.AreEqual(result.Links[2].Index, 52); From ea6704ca1d528df6b94028aa2f82836e16947afe Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 04:41:53 -0400 Subject: [PATCH 202/555] Move parsing --- osu.Game/Online/Chat/MessageFormatter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 7a3941038e..10a005d71a 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -256,9 +256,6 @@ namespace osu.Game.Online.Chat private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { - // see: https://github.com/ppy/osu/pull/24190 - toFormat = Regex.Replace(toFormat, emoji_regex.ToString(), "[emoji]"); - var result = new MessageFormatterResult(toFormat); // handle the [link display] format @@ -282,6 +279,9 @@ namespace osu.Game.Online.Chat // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); + // see: https://github.com/ppy/osu/pull/24190 + result.Text = Regex.Replace(result.Text, emoji_regex.ToString(), "[emoji]"); + return result; } From f5c472c0fe8a2a2bd9f4d9305be46353119dfce0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 17:36:52 +0900 Subject: [PATCH 203/555] Don't hide mod select overlay when pressing select binding with no search --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 4cb6899ebc..9561a8875c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -542,7 +542,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty); AddStep("press enter", () => InputManager.Key(Key.Enter)); - AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); + AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible); } [Test] diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9035503723..c09668850a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -48,7 +48,8 @@ namespace osu.Game.Overlays.Mods /// Contrary to and , the instances /// inside the objects are owned solely by this instance. /// - public Bindable>> AvailableMods { get; } = new Bindable>>(new Dictionary>()); + public Bindable>> AvailableMods { get; } = + new Bindable>>(new Dictionary>()); private Func isValidMod = _ => true; @@ -636,12 +637,9 @@ namespace osu.Game.Overlays.Mods case GlobalAction.Select: { - // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty. + // Pressing select should select first filtered mod if a search is in progress. if (string.IsNullOrEmpty(SearchTerm)) - { - hideOverlay(true); return true; - } ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible); From db37de45ac12ebb0ceb9dda45e40d2e75f92c81c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 17:49:06 +0900 Subject: [PATCH 204/555] Allow saving changes to presets in popover using "select" binding --- .../UserInterface/TestSceneModPresetColumn.cs | 9 ++---- .../Graphics/UserInterfaceV2/OsuPopover.cs | 2 +- osu.Game/Overlays/Mods/AddPresetPopover.cs | 14 ++++++++++ osu.Game/Overlays/Mods/EditPresetPopover.cs | 28 ++++++++++++++----- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index bf6d8e524f..1779b240cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -9,8 +9,8 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -304,11 +304,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName); AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); - AddStep("attempt preset edit", () => - { - InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); - InputManager.Click(MouseButton.Left); - }); + AddStep("commit changes to textbox", () => InputManager.Key(Key.Enter)); + AddStep("attempt preset edit via select binding", () => InputManager.Key(Key.Enter)); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName); } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 00e5b8838c..381193d539 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -65,7 +65,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 return base.OnKeyDown(e); } - public bool OnPressed(KeyBindingPressEvent e) + public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index ef855f6166..638592a9b5 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -8,10 +8,12 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -95,6 +97,18 @@ namespace osu.Game.Overlays.Mods }, true); } + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Select: + createButton.TriggerClick(); + return true; + } + + return base.OnPressed(e); + } + private void createPreset() { realm.Write(r => r.Add(new ModPreset diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 5220f6a391..d755825a95 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -8,11 +8,13 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osuTK; @@ -130,6 +132,25 @@ namespace osu.Game.Overlays.Mods }, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Select: + saveButton.TriggerClick(); + return true; + } + + return base.OnPressed(e); + } + private void useCurrentMods() { saveableMods = selectedMods.Value.ToHashSet(); @@ -150,13 +171,6 @@ namespace osu.Game.Overlays.Mods return !saveableMods.SetEquals(selectedMods.Value); } - protected override void LoadComplete() - { - base.LoadComplete(); - - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); - } - private void save() { preset.PerformWrite(s => From 71351d898247d0329dca7726e0bc3965edb8dffd Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Thu, 13 Jul 2023 05:06:23 -0400 Subject: [PATCH 205/555] I forgor to remove these --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 4d18a3d0bb..46dc47bf53 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -508,14 +508,6 @@ namespace osu.Game.Tests.Chat Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent); Assert.AreEqual(result.Links.Count, 0); - Assert.AreEqual(result.Links[0].Index, 11); - Assert.AreEqual(result.Links[1].Index, 49); - Assert.AreEqual(result.Links[2].Index, 52); - Assert.AreEqual(result.Links[3].Index, 56); - Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12"); - Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10"); - Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00"); - Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20"); } [Test] From 6529c810fadf3c2c25dec35441ca7f72d2448bb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 18:17:46 +0900 Subject: [PATCH 206/555] Change order and test population in `TestSceneStatisticsPanel` to give better visible results --- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 67211a3b72..69c9fcb637 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -30,19 +30,19 @@ namespace osu.Game.Tests.Visual.Ranking public partial class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScoreWithTimeStatistics() + public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + score.HitEvents = createPositionDistributedHitEvents(); loadPanel(score); } [Test] - public void TestScoreWithPositionStatistics() + public void TestScoreWithTimeStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); loadPanel(score); } @@ -89,18 +89,19 @@ namespace osu.Game.Tests.Visual.Ranking private static List createPositionDistributedHitEvents() { - var hitEvents = new List(); + var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + // Use constant seed for reproducibility var random = new Random(0); - for (int i = 0; i < 500; i++) + for (int i = 0; i < hitEvents.Count; i++) { double angle = random.NextDouble() * 2 * Math.PI; double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle))); - hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); + hitEvents[i] = hitEvents[i].With(position); } return hitEvents; From b7ab46d87b538725086ab4d556f4f19d4118ddd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:06:26 +0900 Subject: [PATCH 207/555] Add full statistics score to `TestSceneResultsScreen` --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 5 ++++- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index c5b61c1a90..4eba60de3c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -82,7 +82,10 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both }; - stack.Push(screen = createResultsScreen()); + var score = TestResources.CreateTestScoreInfo(); + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + + stack.Push(screen = createResultsScreen(score)); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay not present", () => screen.RetryOverlay == null); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 69c9fcb637..e02782678f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private static List createPositionDistributedHitEvents() + public static List CreatePositionDistributedHitEvents() { var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); From aebbffacf247adf4d9e64e3205c5c5c421032685 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 19:06:48 +0900 Subject: [PATCH 208/555] Show online stats on `TestSceneStatisticsPanel` Fix test regression --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 1 + osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4eba60de3c..937bbe1448 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -83,6 +83,7 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e02782678f..e0d743b046 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -33,6 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); @@ -79,11 +82,12 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel + Child = new SoloStatisticsPanel(score) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, - Score = { Value = score } + Score = { Value = score }, + StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } }; }); From 654a7057fca18ba68bb8590b34b4a06076d014ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:29:58 +0900 Subject: [PATCH 209/555] Add actual statistics changes to better visualise layout --- .../Ranking/TestSceneStatisticsPanel.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e0d743b046..93005271a9 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -87,7 +87,44 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, - StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } + StatisticsUpdate = + { + Value = new SoloStatisticsUpdate(score, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 20, + }, + GlobalRank = 38000, + CountryRank = 12006, + PP = 2134, + RankedScore = 21123849, + Accuracy = 0.985, + PlayCount = 13375, + PlayTime = 354490, + TotalScore = 128749597, + TotalHits = 0, + MaxCombo = 1233, + }, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 30, + }, + GlobalRank = 36000, + CountryRank = 12000, + PP = (decimal)2134.5, + RankedScore = 23897015, + Accuracy = 0.984, + PlayCount = 13376, + PlayTime = 35789, + TotalScore = 132218497, + TotalHits = 0, + MaxCombo = 1233, + }) + } }; }); From 6ef39b87fed8b44b5f9219da12d639ae5e4f422b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:24:09 +0900 Subject: [PATCH 210/555] Reorder tests for testability --- .../Visual/Ranking/TestSceneResultsScreen.cs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 937bbe1448..1c904a936f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -69,6 +69,30 @@ namespace osu.Game.Tests.Visual.Ranking })); } + [TestCase(1, ScoreRank.X)] + [TestCase(0.9999, ScoreRank.S)] + [TestCase(0.975, ScoreRank.S)] + [TestCase(0.925, ScoreRank.A)] + [TestCase(0.85, ScoreRank.B)] + [TestCase(0.75, ScoreRank.C)] + [TestCase(0.5, ScoreRank.D)] + [TestCase(0.2, ScoreRank.D)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + { + TestResultsScreen screen = null; + + var score = TestResources.CreateTestScoreInfo(); + + score.OnlineID = 1234; + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + score.Accuracy = accuracy; + score.Rank = rank; + + loadResultsScreen(() => screen = createResultsScreen(score)); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddAssert("retry overlay present", () => screen.RetryOverlay != null); + } + [Test] public void TestResultsWithoutPlayer() { @@ -83,8 +107,6 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); - score.OnlineID = 1234; - score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); }); @@ -92,28 +114,6 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay not present", () => screen.RetryOverlay == null); } - [TestCase(0.2, ScoreRank.D)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(1, ScoreRank.X)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) - { - TestResultsScreen screen = null; - - var score = TestResources.CreateTestScoreInfo(); - - score.Accuracy = accuracy; - score.Rank = rank; - - loadResultsScreen(() => screen = createResultsScreen(score)); - AddUntilStep("wait for loaded", () => screen.IsLoaded); - AddAssert("retry overlay present", () => screen.RetryOverlay != null); - } - [Test] public void TestResultsForUnranked() { @@ -332,13 +332,14 @@ namespace osu.Game.Tests.Visual.Ranking } } - private partial class TestResultsScreen : ResultsScreen + private partial class TestResultsScreen : SoloResultsScreen { public HotkeyRetryOverlay RetryOverlay; public TestResultsScreen(ScoreInfo score) : base(score, true) { + ShowUserStatistics = true; } protected override void LoadComplete() From 0173543dcc2f2d639ba8ab74c3976c6a19d0baf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 18:17:46 +0900 Subject: [PATCH 211/555] Change order and test population in `TestSceneStatisticsPanel` to give better visible results --- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 67211a3b72..69c9fcb637 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -30,19 +30,19 @@ namespace osu.Game.Tests.Visual.Ranking public partial class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScoreWithTimeStatistics() + public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + score.HitEvents = createPositionDistributedHitEvents(); loadPanel(score); } [Test] - public void TestScoreWithPositionStatistics() + public void TestScoreWithTimeStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); loadPanel(score); } @@ -89,18 +89,19 @@ namespace osu.Game.Tests.Visual.Ranking private static List createPositionDistributedHitEvents() { - var hitEvents = new List(); + var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + // Use constant seed for reproducibility var random = new Random(0); - for (int i = 0; i < 500; i++) + for (int i = 0; i < hitEvents.Count; i++) { double angle = random.NextDouble() * 2 * Math.PI; double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle))); - hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); + hitEvents[i] = hitEvents[i].With(position); } return hitEvents; From 6e9785ec589fe7f586d213b4cb17242dbb6360eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:06:26 +0900 Subject: [PATCH 212/555] Add full statistics score to `TestSceneResultsScreen` --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 5 ++++- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index c5b61c1a90..4eba60de3c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -82,7 +82,10 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both }; - stack.Push(screen = createResultsScreen()); + var score = TestResources.CreateTestScoreInfo(); + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + + stack.Push(screen = createResultsScreen(score)); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay not present", () => screen.RetryOverlay == null); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 69c9fcb637..e02782678f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private static List createPositionDistributedHitEvents() + public static List CreatePositionDistributedHitEvents() { var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); From b43949300bdee77effee0e6c5bfb5dc8daf012c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 19:06:48 +0900 Subject: [PATCH 213/555] Show online stats on `TestSceneStatisticsPanel` Fix test regression --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 1 + osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4eba60de3c..937bbe1448 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -83,6 +83,7 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e02782678f..e0d743b046 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -33,6 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); @@ -79,11 +82,12 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel + Child = new SoloStatisticsPanel(score) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, - Score = { Value = score } + Score = { Value = score }, + StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } }; }); From 48670308a501205d518ba1cb086e64cc4e1dfcdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:29:58 +0900 Subject: [PATCH 214/555] Add actual statistics changes to better visualise layout --- .../Ranking/TestSceneStatisticsPanel.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e0d743b046..93005271a9 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -87,7 +87,44 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, - StatisticsUpdate = { Value = new SoloStatisticsUpdate(score, new UserStatistics(), new UserStatistics()) } + StatisticsUpdate = + { + Value = new SoloStatisticsUpdate(score, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 20, + }, + GlobalRank = 38000, + CountryRank = 12006, + PP = 2134, + RankedScore = 21123849, + Accuracy = 0.985, + PlayCount = 13375, + PlayTime = 354490, + TotalScore = 128749597, + TotalHits = 0, + MaxCombo = 1233, + }, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 30, + }, + GlobalRank = 36000, + CountryRank = 12000, + PP = (decimal)2134.5, + RankedScore = 23897015, + Accuracy = 0.984, + PlayCount = 13376, + PlayTime = 35789, + TotalScore = 132218497, + TotalHits = 0, + MaxCombo = 1233, + }) + } }; }); From 8c4831e09f6c8acd1cd0919b2990dbcba0bfb45a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:24:09 +0900 Subject: [PATCH 215/555] Reorder tests for testability --- .../Visual/Ranking/TestSceneResultsScreen.cs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 937bbe1448..1c904a936f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -69,6 +69,30 @@ namespace osu.Game.Tests.Visual.Ranking })); } + [TestCase(1, ScoreRank.X)] + [TestCase(0.9999, ScoreRank.S)] + [TestCase(0.975, ScoreRank.S)] + [TestCase(0.925, ScoreRank.A)] + [TestCase(0.85, ScoreRank.B)] + [TestCase(0.75, ScoreRank.C)] + [TestCase(0.5, ScoreRank.D)] + [TestCase(0.2, ScoreRank.D)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + { + TestResultsScreen screen = null; + + var score = TestResources.CreateTestScoreInfo(); + + score.OnlineID = 1234; + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + score.Accuracy = accuracy; + score.Rank = rank; + + loadResultsScreen(() => screen = createResultsScreen(score)); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddAssert("retry overlay present", () => screen.RetryOverlay != null); + } + [Test] public void TestResultsWithoutPlayer() { @@ -83,8 +107,6 @@ namespace osu.Game.Tests.Visual.Ranking }; var score = TestResources.CreateTestScoreInfo(); - score.OnlineID = 1234; - score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); stack.Push(screen = createResultsScreen(score)); }); @@ -92,28 +114,6 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay not present", () => screen.RetryOverlay == null); } - [TestCase(0.2, ScoreRank.D)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(1, ScoreRank.X)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) - { - TestResultsScreen screen = null; - - var score = TestResources.CreateTestScoreInfo(); - - score.Accuracy = accuracy; - score.Rank = rank; - - loadResultsScreen(() => screen = createResultsScreen(score)); - AddUntilStep("wait for loaded", () => screen.IsLoaded); - AddAssert("retry overlay present", () => screen.RetryOverlay != null); - } - [Test] public void TestResultsForUnranked() { @@ -332,13 +332,14 @@ namespace osu.Game.Tests.Visual.Ranking } } - private partial class TestResultsScreen : ResultsScreen + private partial class TestResultsScreen : SoloResultsScreen { public HotkeyRetryOverlay RetryOverlay; public TestResultsScreen(ScoreInfo score) : base(score, true) { + ShowUserStatistics = true; } protected override void LoadComplete() From 98bf15182efada7ceb1b9d9fa2b1ca9a6bef02f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 18:17:57 +0900 Subject: [PATCH 216/555] Remove more `GridContainer` nonsense --- .../Ranking/Statistics/StatisticContainer.cs | 29 ++++++------------- .../Ranking/Statistics/StatisticsPanel.cs | 24 ++++----------- 2 files changed, 15 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 9191ee6f52..f3308e9931 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -26,31 +26,20 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new GridContainer + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] + Children = new[] { - new[] + createHeader(item), + new Container { - createHeader(item) - }, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 15 }, - Child = item.CreateContent() - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 15 }, + Child = item.CreateContent() + } } }; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8b059efaf4..40d2d29902 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Ranking.Statistics } else { - FillFlowContainer rows; + FillFlowContainer flow; container = new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.Both, @@ -133,11 +133,12 @@ namespace osu.Game.Screens.Ranking.Statistics Alpha = 0, Children = new[] { - rows = new FillFlowContainer + flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(30, 15) + Spacing = new Vector2(30, 15), + Direction = FillDirection.Full, } } }; @@ -146,35 +147,22 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (var item in statisticItems) { - var columnContent = new List(); - if (!hitEventsAvailable && item.RequiresHitEvents) { anyRequiredHitEvents = true; continue; } - columnContent.Add(new StatisticContainer(item) + flow.Add(new StatisticContainer(item) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - - rows.Add(new GridContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { columnContent.ToArray() }, - ColumnDimensions = new[] { new Dimension() }, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); } if (anyRequiredHitEvents) { - rows.Add(new FillFlowContainer + flow.Add(new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, From 7637a9e60394c79d3e1f1155b896ef75f8793cd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 18:16:09 +0900 Subject: [PATCH 217/555] Adjust metrics of `OverallRanking` --- .../Ranking/Statistics/User/OverallRanking.cs | 2 +- .../Statistics/User/RankingChangeRow.cs | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 447f206128..1d39023223 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Spacing = new Vector2(5), Children = new Drawable[] { new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index 5348b4a522..534e1e58cd 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -46,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User new OsuSpriteText { Text = Label, - Font = OsuFont.Default.With(size: 18) + Font = OsuFont.Default.With(size: 14) }, new FillFlowContainer { @@ -65,17 +66,31 @@ namespace osu.Game.Screens.Ranking.Statistics.User Spacing = new Vector2(5), Children = new Drawable[] { - changeIcon = new SpriteIcon + new Container { + Size = new Vector2(14), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Size = new Vector2(18) + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1 + }, + changeIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(10), + }, + } }, currentValueText = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Default.With(size: 18, weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: 14, weight: FontWeight.Bold) }, } }, @@ -123,7 +138,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User } else { - comparisonColour = colours.Orange1; + comparisonColour = colours.Gray4; icon = FontAwesome.Solid.Minus; } From f223fd7c3bf5bd19b36b8abe613e6fd549576412 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 18:16:15 +0900 Subject: [PATCH 218/555] Adjust metrics of `PerformanceBreakdown` --- .../Ranking/Statistics/PerformanceBreakdownChart.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index 10cb77fa91..6979f0ad94 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Text = "Achieved PP", Colour = Color4Extensions.FromHex("#66FFCC") }, @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), Colour = Color4Extensions.FromHex("#66FFCC") } }, @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Text = "Maximum", Colour = OsuColour.Gray(0.7f) }, @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Colour = OsuColour.Gray(0.7f) } } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Text = attribute.DisplayName, Colour = Colour4.White }, @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), Text = percentage.ToLocalisableString("0%"), Colour = Colour4.White } From 4f089eb5a5d7d471d2d218a68df32c996c58f45a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jul 2023 18:16:55 +0900 Subject: [PATCH 219/555] Adjust metrics of `AccuracyHeatmap` --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 5d2f6a14c7..6564a086fe 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; @@ -120,18 +121,22 @@ namespace osu.Game.Rulesets.Osu.Statistics new OsuSpriteText { Text = "Overshoot", + Font = OsuFont.GetFont(size: 12), Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Padding = new MarginPadding(3), + Origin = Anchor.BottomLeft, + Padding = new MarginPadding(2), + Rotation = -rotation, RelativePositionAxes = Axes.Both, Y = -(inner_portion + line_extension) / 2, }, new OsuSpriteText { Text = "Undershoot", + Font = OsuFont.GetFont(size: 12), Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(3), + Origin = Anchor.TopRight, + Rotation = -rotation, + Padding = new MarginPadding(2), RelativePositionAxes = Axes.Both, Y = (inner_portion + line_extension) / 2, }, From d54cf639839f847e60fcc56202f322d0f59d0ad6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:29:20 +0900 Subject: [PATCH 220/555] Centralise font size specification for statistic items (and reduce slightly) --- .../Statistics/HitEventTimingDistributionGraph.cs | 8 ++++---- .../Ranking/Statistics/PerformanceBreakdownChart.cs | 12 ++++++------ .../Ranking/Statistics/SimpleStatisticItem.cs | 4 ++-- .../Screens/Ranking/Statistics/StatisticContainer.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 5 +++++ .../Ranking/Statistics/User/RankingChangeRow.cs | 4 ++-- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index e8d2eefd81..fb9a78fb54 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Ranking.Statistics axisFlow = new Container { RelativeSizeAxes = Axes.X, - Height = axis_font_size, + Height = StatisticItem.FONT_SIZE, } }, }, @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Ranking.Statistics Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "0", - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) }); for (int i = 1; i <= axis_points; i++) @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Ranking.Statistics X = -position / 2, Alpha = alpha, Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) }); axisFlow.Add(new OsuSpriteText @@ -202,7 +202,7 @@ namespace osu.Game.Screens.Ranking.Statistics X = position / 2, Alpha = alpha, Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) }); } } diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index 6979f0ad94..ee0ce6183d 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = "Achieved PP", Colour = Color4Extensions.FromHex("#66FFCC") }, @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE), Colour = Color4Extensions.FromHex("#66FFCC") } }, @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = "Maximum", Colour = OsuColour.Gray(0.7f) }, @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Colour = OsuColour.Gray(0.7f) } } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = attribute.DisplayName, Colour = Colour4.White }, @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE), Text = percentage.ToLocalisableString("0%"), Colour = Colour4.White } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index 99f4e1e342..23ccc3d0b7 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -44,13 +44,13 @@ namespace osu.Game.Screens.Ranking.Statistics Text = Name, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 14) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE) }, value = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) } }); } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index f3308e9931..c3cc443adb 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Ranking.Statistics Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold), } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index fd7a0ddb4f..cf95886905 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -12,6 +12,11 @@ namespace osu.Game.Screens.Ranking.Statistics /// public class StatisticItem { + /// + /// The recommended font size to use in statistic items to make sure they match others. + /// + public const float FONT_SIZE = 13; + /// /// The name of this item. /// diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index 534e1e58cd..a58e028baa 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User new OsuSpriteText { Text = Label, - Font = OsuFont.Default.With(size: 14) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE) }, new FillFlowContainer { @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Default.With(size: 14, weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) }, } }, From 6edaf4f230c4b0ab82f6ca8dbab0e3774cdec1a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 17:52:08 +0900 Subject: [PATCH 221/555] Ensure `PerformanceBreakdown` pieces cannot be null --- .../Rulesets/Difficulty/PerformanceBreakdown.cs | 13 +++++++++++-- .../Difficulty/PerformanceBreakdownCalculator.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs index bd971db476..6e41855ca3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Difficulty { /// @@ -19,5 +17,16 @@ namespace osu.Game.Rulesets.Difficulty /// Performance of a perfect play for comparison. /// public PerformanceAttributes PerfectPerformance { get; set; } + + /// + /// Create a new performance breakdown. + /// + /// Actual gameplay performance. + /// Performance of a perfect play for comparison. + public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance) + { + Performance = performance; + PerfectPerformance = perfectPerformance; + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 8b59500f43..ad9257d4f3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Difficulty getPerfectPerformance(score, cancellationToken) ).ConfigureAwait(false); - return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] }; + return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes()); } [ItemCanBeNull] From 947b40149f3e7581f15d40d6daeccf772bf8ac61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:30:05 +0900 Subject: [PATCH 222/555] Adjust metrics of `SimpleStatisticTable` --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index bd6ab4086b..620009e182 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Mania RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(string.Empty, () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 036d13c5aa..3b24cc5997 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -319,7 +319,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(string.Empty, () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index d68df4558a..4eb4979724 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -103,7 +103,6 @@ namespace osu.Game.Screens.Ranking.Statistics public Spacer() { RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding { Vertical = 4 }; InternalChild = new CircularContainer { From 0881f4772cb730855cb8ebc5ec8c27327bd8dc60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:31:45 +0900 Subject: [PATCH 223/555] Adjust metrics of `HitEventTimingDistributionGraph` --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index fb9a78fb54..1260ec2339 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -131,14 +131,11 @@ namespace osu.Game.Screens.Ranking.Statistics Container axisFlow; - const float axis_font_size = 12; + Padding = new MarginPadding { Horizontal = 5 }; InternalChild = new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Width = 0.8f, Content = new[] { new Drawable[] From 1a7b00ec152c15f467b05e1995bbfd32c7267403 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Jun 2023 17:52:08 +0900 Subject: [PATCH 224/555] Ensure `PerformanceBreakdown` pieces cannot be null --- .../Rulesets/Difficulty/PerformanceBreakdown.cs | 13 +++++++++++-- .../Difficulty/PerformanceBreakdownCalculator.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs index bd971db476..6e41855ca3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Difficulty { /// @@ -19,5 +17,16 @@ namespace osu.Game.Rulesets.Difficulty /// Performance of a perfect play for comparison. /// public PerformanceAttributes PerfectPerformance { get; set; } + + /// + /// Create a new performance breakdown. + /// + /// Actual gameplay performance. + /// Performance of a perfect play for comparison. + public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance) + { + Performance = performance; + PerfectPerformance = perfectPerformance; + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 8b59500f43..ad9257d4f3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Difficulty getPerfectPerformance(score, cancellationToken) ).ConfigureAwait(false); - return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] }; + return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes()); } [ItemCanBeNull] From b333945cde705b12b8de3b32838cf532dc3a9a6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 01:58:38 +0900 Subject: [PATCH 225/555] Change `OverallRanking` to use a two-column layout similar to statistics table --- .../Statistics/SimpleStatisticTable.cs | 2 +- .../Ranking/Statistics/User/OverallRanking.cs | 49 ++++++++++++++----- .../Statistics/User/RankingChangeRow.cs | 5 +- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index 4eb4979724..ed31bc8643 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Ranking.Statistics Direction = FillDirection.Vertical }; - private partial class Spacer : CompositeDrawable + public partial class Spacer : CompositeDrawable { public Spacer() { diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 1d39023223..d08a654e99 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Solo; -using osuTK; namespace osu.Game.Screens.Ranking.Statistics.User { @@ -18,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User public Bindable StatisticsUpdate { get; } = new Bindable(); private LoadingLayer loadingLayer = null!; - private FillFlowContainer content = null!; + private GridContainer content = null!; [BackgroundDependencyLoader] private void load() @@ -33,21 +32,47 @@ namespace osu.Game.Screens.Ranking.Statistics.User { RelativeSizeAxes = Axes.Both, }, - content = new FillFlowContainer + content = new GridContainer { AlwaysPresent = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Children = new Drawable[] + ColumnDimensions = new[] { - new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } } + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + }, + new Drawable[] { }, + new Drawable[] + { + new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + }, + new Drawable[] { }, + new Drawable[] + { + new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + } } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index a58e028baa..906bf8d5ca 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -54,7 +54,8 @@ namespace osu.Game.Screens.Ranking.Statistics.User Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = StatisticItem.FONT_SIZE * 2, Children = new Drawable[] { new FillFlowContainer @@ -98,7 +99,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Font = OsuFont.Default.With(weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) } } } From 2073810e958b7376d4797de1dc1e5d79213be7d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:48:24 +0900 Subject: [PATCH 226/555] Add performance breakdown chart for osu!catch --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index e51e5cc5db..bd6b857fe8 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch @@ -218,5 +220,17 @@ namespace osu.Game.Rulesets.Catch public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); + + public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + }; + } } } From 9ad63bae374262f1a1158dcc1975eecda1595e39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:50:52 +0900 Subject: [PATCH 227/555] Add missing heading for statistics section in results screen --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index bd6ab4086b..e46c85ab29 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Mania RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 036d13c5aa..c00bab8ed7 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -319,7 +319,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index de3fa1750f..feb8b0c2c2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) From e34839c8918ef5fbda6fe2b547c4963feceed223 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 18:55:54 +0900 Subject: [PATCH 228/555] Rename `StatisticContainer` to `StatisticItemContainer` and add a background --- ...Container.cs => StatisticItemContainer.cs} | 43 ++++++++++++++----- .../Ranking/Statistics/StatisticsPanel.cs | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) rename osu.Game/Screens/Ranking/Statistics/{StatisticContainer.cs => StatisticItemContainer.cs} (62%) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs similarity index 62% rename from osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs rename to osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs index c3cc443adb..6e18ae1fe4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; @@ -15,31 +16,53 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Wraps a to add a header and suitable layout for use in . /// - internal partial class StatisticContainer : CompositeDrawable + internal partial class StatisticItemContainer : CompositeDrawable { /// - /// Creates a new . + /// Creates a new . /// /// The to display. - public StatisticContainer(StatisticItem item) + public StatisticItemContainer(StatisticItem item) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + Padding = new MarginPadding(5); + + InternalChild = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new[] + Masking = true, + CornerRadius = 6, + Children = new Drawable[] { - createHeader(item), + new Box + { + Colour = ColourInfo.GradientVertical( + OsuColour.Gray(0.25f), + OsuColour.Gray(0.18f) + ), + Alpha = 0.95f, + RelativeSizeAxes = Axes.Both, + }, new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 15 }, - Child = item.CreateContent() - } + Padding = new MarginPadding(5), + Children = new[] + { + createHeader(item), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10) { Top = 30 }, + Child = item.CreateContent() + } + } + }, } }; } @@ -52,7 +75,7 @@ namespace osu.Game.Screens.Ranking.Statistics return new FillFlowContainer { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = 20, Direction = FillDirection.Horizontal, Spacing = new Vector2(5, 0), Children = new Drawable[] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 40d2d29902..8ab774f76d 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -153,7 +153,7 @@ namespace osu.Game.Screens.Ranking.Statistics continue; } - flow.Add(new StatisticContainer(item) + flow.Add(new StatisticItemContainer(item) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 2c27b17c85a14a39bc4dbdc0e5297945d6e0e838 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 19:00:03 +0900 Subject: [PATCH 229/555] Disable masking on inner scroll container --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8ab774f76d..9357d1f385 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -130,6 +130,8 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Masking = false, + ScrollbarOverlapsContent = false, Alpha = 0, Children = new[] { From cde8d8e7f1fccc221527209216c24a73612e7d71 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 13 Jul 2023 14:33:21 +0200 Subject: [PATCH 230/555] Optimize realm transactions for editor Save action --- osu.Game/Beatmaps/BeatmapManager.cs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 54c243e842..265149e378 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -431,8 +431,9 @@ namespace osu.Game.Beatmaps beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; beatmapInfo.ResetOnlineInfo(); - using (var stream = new MemoryStream()) + Realm.Write(r => { + using var stream = new MemoryStream(); using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); @@ -458,23 +459,20 @@ namespace osu.Game.Beatmaps updateHashAndMarkDirty(setInfo); - Realm.Write(r => - { - var liveBeatmapSet = r.Find(setInfo.ID)!; + var liveBeatmapSet = r.Find(setInfo.ID)!; - setInfo.CopyChangesToRealm(liveBeatmapSet); + setInfo.CopyChangesToRealm(liveBeatmapSet); - if (transferCollections) - beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); + if (transferCollections) + beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); - liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) - .UpdateLocalScores(r); + liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) + .UpdateLocalScores(r); - // do not look up metadata. - // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. - ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); - }); - } + // do not look up metadata. + // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. + ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); + }); Debug.Assert(beatmapInfo.BeatmapSet != null); From 20e4e2581a530dc020d0b8c3252ba961110ee04e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 22:12:55 +0900 Subject: [PATCH 231/555] Change `IBeatSyncProvider.Clock` to always be non-null --- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 5 ----- osu.Game/Beatmaps/BeatSyncProviderExtensions.cs | 7 +------ osu.Game/Beatmaps/IBeatSyncProvider.cs | 2 +- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 5 +---- osu.Game/Rulesets/Mods/MetronomeBeat.cs | 2 +- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 4 ++-- osu.Game/Screens/Edit/Timing/TapButton.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 3 +-- .../Storyboards/Drawables/DrawableStoryboardAnimation.cs | 2 +- 9 files changed, 9 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 5d97714ab5..c723610614 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Audio.Track; @@ -283,8 +282,6 @@ namespace osu.Game.Tests.Visual.UserInterface if (ReferenceEquals(timingPoints[^1], current)) { - Debug.Assert(BeatSyncSource.Clock != null); - return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength); } @@ -295,8 +292,6 @@ namespace osu.Game.Tests.Visual.UserInterface { base.Update(); - Debug.Assert(BeatSyncSource.Clock != null); - timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; currentTime.Value = BeatSyncSource.Clock.CurrentTime; diff --git a/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs index 767aa5df73..e2b805bf0d 100644 --- a/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs +++ b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs @@ -5,14 +5,9 @@ namespace osu.Game.Beatmaps { public static class BeatSyncProviderExtensions { - /// - /// Check whether beat sync is currently available. - /// - public static bool CheckBeatSyncAvailable(this IBeatSyncProvider provider) => provider.Clock != null; - /// /// Whether the beat sync provider is currently in a kiai section. Should make everything more epic. /// - public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.Clock != null && provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; + public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; } } diff --git a/osu.Game/Beatmaps/IBeatSyncProvider.cs b/osu.Game/Beatmaps/IBeatSyncProvider.cs index 9ee19e720d..776552cfa5 100644 --- a/osu.Game/Beatmaps/IBeatSyncProvider.cs +++ b/osu.Game/Beatmaps/IBeatSyncProvider.cs @@ -24,6 +24,6 @@ namespace osu.Game.Beatmaps /// /// Access a clock currently responsible for providing beat sync. If null, no current provider is available. /// - IClock? Clock { get; } + IClock Clock { get; } } } diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 42b30f9d18..f911311a09 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; @@ -86,14 +85,12 @@ namespace osu.Game.Graphics.Containers TimingControlPoint timingPoint; EffectControlPoint effectPoint; - IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true; + IsBeatSyncedWithTrack = BeatSyncSource.Clock.IsRunning; double currentTrackTime; if (IsBeatSyncedWithTrack) { - Debug.Assert(BeatSyncSource.Clock != null); - currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; diff --git a/osu.Game/Rulesets/Mods/MetronomeBeat.cs b/osu.Game/Rulesets/Mods/MetronomeBeat.cs index 265970ea46..5615362d1a 100644 --- a/osu.Game/Rulesets/Mods/MetronomeBeat.cs +++ b/osu.Game/Rulesets/Mods/MetronomeBeat.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mods int timeSignature = timingPoint.TimeSignature.Numerator; // play metronome from one measure before the first object. - if (BeatSyncSource.Clock?.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) + if (BeatSyncSource.Clock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) return; sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f; diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f4a39405a1..9f03281d0c 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -240,7 +240,7 @@ namespace osu.Game.Screens.Edit.Timing { base.Update(); - if (BeatSyncSource.ControlPoints == null || BeatSyncSource.Clock == null) + if (BeatSyncSource.ControlPoints == null) return; metronomeClock.Rate = IsBeatSyncedWithTrack ? BeatSyncSource.Clock.Rate : 1; @@ -259,7 +259,7 @@ namespace osu.Game.Screens.Edit.Timing this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); } - if (BeatSyncSource.Clock?.IsRunning != true && isSwinging) + if (!BeatSyncSource.Clock.IsRunning && isSwinging) { swing.ClearTransforms(true); diff --git a/osu.Game/Screens/Edit/Timing/TapButton.cs b/osu.Game/Screens/Edit/Timing/TapButton.cs index f28c6ccf0a..fd60fb1b5b 100644 --- a/osu.Game/Screens/Edit/Timing/TapButton.cs +++ b/osu.Game/Screens/Edit/Timing/TapButton.cs @@ -310,7 +310,7 @@ namespace osu.Game.Screens.Edit.Timing } double averageBeatLength = (tapTimings.Last() - tapTimings.Skip(initial_taps_to_ignore).First()) / (tapTimings.Count - initial_taps_to_ignore - 1); - double clockRate = beatSyncSource?.Clock?.Rate ?? 1; + double clockRate = beatSyncSource?.Clock.Rate ?? 1; double bpm = Math.Round(60000 / averageBeatLength / clockRate); diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 5000a97b3d..fa26cfab46 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -102,8 +102,7 @@ namespace osu.Game.Screens.Menu for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; - if (beatSyncProvider.Clock != null) - addAmplitudesFromSource(beatSyncProvider); + addAmplitudesFromSource(beatSyncProvider); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index be77c9a98e..82c01ea6a1 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -128,7 +128,7 @@ namespace osu.Game.Storyboards.Drawables // // In the case of storyboard animations, we want to synchronise with game time perfectly // so let's get a correct time based on gameplay clock and earliest transform. - PlaybackPosition = (beatSyncProvider.Clock?.CurrentTime ?? Clock.CurrentTime) - Animation.EarliestTransformTime; + PlaybackPosition = beatSyncProvider.Clock.CurrentTime - Animation.EarliestTransformTime; } private void skinSourceChanged() From 1698caa078e12a33e808ba5b86c00f3c805e5bb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 20:31:00 +0900 Subject: [PATCH 232/555] Add kiai fountains to main menu --- .../Visual/Menus/TestSceneStarFountain.cs | 52 +++++++++ osu.Game/Screens/Menu/KiaiMenuFountains.cs | 67 ++++++++++++ osu.Game/Screens/Menu/MainMenu.cs | 1 + osu.Game/Screens/Menu/StarFountain.cs | 102 ++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs create mode 100644 osu.Game/Screens/Menu/KiaiMenuFountains.cs create mode 100644 osu.Game/Screens/Menu/StarFountain.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs new file mode 100644 index 0000000000..b12f3e7946 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public partial class TestSceneStarFountain : OsuTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + AddStep("make fountains", () => + { + Children = new[] + { + new StarFountain + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + X = 200, + }, + new StarFountain + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + X = -200, + }, + }; + }); + } + + [Test] + public void TestPew() + { + AddRepeatStep("activate fountains sometimes", () => + { + foreach (var fountain in Children.OfType()) + { + if (RNG.NextSingle() > 0.8f) + fountain.Shoot(); + } + }, 150); + } + } +} diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs new file mode 100644 index 0000000000..a4d58e398a --- /dev/null +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -0,0 +1,67 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.Menu +{ + public partial class KiaiMenuFountains : BeatSyncedContainer + { + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + Children = new[] + { + new StarFountain + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = 250, + }, + new StarFountain + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -250, + }, + }; + } + + private bool isTriggered; + + private double? lastTrigger; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (effectPoint.KiaiMode && !isTriggered) + { + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; + if (isNearEffectPoint) + Shoot(); + } + + isTriggered = effectPoint.KiaiMode; + } + + public void Shoot() + { + if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) + return; + + foreach (var fountain in Children.OfType()) + fountain.Shoot(); + + lastTrigger = Clock.CurrentTime; + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index f34a1954a5..baa34d4914 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -136,6 +136,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.TopRight, Margin = new MarginPadding { Right = 15, Top = 5 } }, + new KiaiMenuFountains(), holdToExitGameOverlay?.CreateProxy() ?? Empty() }); diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs new file mode 100644 index 0000000000..e8feba4451 --- /dev/null +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Game.Screens.Menu +{ + public partial class StarFountain : CompositeDrawable + { + private DrawablePool starPool = null!; + private Container starContainer = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + starPool = new DrawablePool(192), + starContainer = new Container() + }; + } + + public void Shoot() + { + int dir = RNG.Next(-1, 2); + + for (int i = 0; i < 192; i++) + { + double offset = i * 3; + + starContainer.Add(starPool.Get(s => + { + s.Velocity = new Vector2(dir * 0.6f + RNG.NextSingle(-0.25f, 0.25f), -RNG.NextSingle(2.2f, 2.4f)); + s.Position = new Vector2(RNG.NextSingle(-5, 5), 50); + s.Hide(); + + using (s.BeginDelayedSequence(offset)) + { + s.FadeIn(); + s.ScaleTo(1); + + double duration = RNG.Next(300, 1300); + + s.FadeOutFromOne(duration, Easing.Out); + s.ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out); + + s.Expire(); + } + })); + } + } + + private partial class Star : PoolableDrawable + { + public Vector2 Velocity = Vector2.Zero; + + private float rotation; + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Scale = new Vector2(0.5f), + Texture = textures.Get("Menu/fountain-star") + } + }; + + rotation = RNG.NextSingle(-2f, 2f); + } + + protected override void Update() + { + const float gravity = 0.004f; + + base.Update(); + + float elapsed = (float)Time.Elapsed; + + Position += Velocity * elapsed; + Velocity += new Vector2(0, elapsed * gravity); + + Rotation += rotation * elapsed; + } + } + } +} From 53fccaa3bd5afeb5413b3cb1537c8808bb386f5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 22:30:27 +0900 Subject: [PATCH 233/555] Use skinnable sprite --- osu.Game/Screens/Menu/StarFountain.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index e8feba4451..f891e989bb 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -5,9 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Menu @@ -64,20 +63,18 @@ namespace osu.Game.Screens.Menu private float rotation; [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load() { AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; InternalChildren = new Drawable[] { - new Sprite + new SkinnableSprite("star2") { Anchor = Anchor.Centre, Origin = Anchor.Centre, Blending = BlendingParameters.Additive, - Scale = new Vector2(0.5f), - Texture = textures.Get("Menu/fountain-star") } }; From 8f826a3702640fedf73b94127528ebd69e9c469d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 22:57:04 +0900 Subject: [PATCH 234/555] Add skinning support for kiai fountain stars --- osu.Game/Screens/Menu/StarFountain.cs | 2 +- osu.Game/Skinning/LegacySkinTransformer.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index f891e989bb..8aedf3199b 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Menu InternalChildren = new Drawable[] { - new SkinnableSprite("star2") + new SkinnableSprite("Menu/fountain-star") { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 367e5bae01..97b0f3a9b5 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; using static osu.Game.Skinning.SkinConfiguration; @@ -23,6 +24,18 @@ namespace osu.Game.Skinning { } + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + switch (componentName) + { + case "fountain-star": + componentName = "star2"; + break; + } + + return base.GetTexture(componentName, wrapModeS, wrapModeT); + } + public override ISample? GetSample(ISampleInfo sampleInfo) { if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) From 1051982bc59d14f937575812e1efe7d659bf24fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jul 2023 23:12:29 +0900 Subject: [PATCH 235/555] Tidy up code with constants --- osu.Game/Screens/Menu/StarFountain.cs | 45 ++++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 8aedf3199b..7fa996e39a 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -28,29 +28,42 @@ namespace osu.Game.Screens.Menu public void Shoot() { - int dir = RNG.Next(-1, 2); + // left centre or right movement. + int direction = RNG.Next(-1, 2); - for (int i = 0; i < 192; i++) + const int total_stars = 192; + + const float x_velocity_from_direction = 0.6f; + const float x_velocity_random_variance = 0.25f; + + const float y_velocity_base = -2.0f; + const float y_velocity_random_variance = 0.25f; + + const float x_spawn_position_variance = 10; + const float y_spawn_position_offset = 50; + + for (int i = 0; i < total_stars; i++) { - double offset = i * 3; + double initialOffset = i * 3; starContainer.Add(starPool.Get(s => { - s.Velocity = new Vector2(dir * 0.6f + RNG.NextSingle(-0.25f, 0.25f), -RNG.NextSingle(2.2f, 2.4f)); - s.Position = new Vector2(RNG.NextSingle(-5, 5), 50); + s.Velocity = new Vector2( + direction * x_velocity_from_direction + getRandomVariance(x_velocity_random_variance), + y_velocity_base + getRandomVariance(y_velocity_random_variance)); + + s.Position = new Vector2(getRandomVariance(x_spawn_position_variance), y_spawn_position_offset); + s.Hide(); - using (s.BeginDelayedSequence(offset)) + using (s.BeginDelayedSequence(initialOffset)) { - s.FadeIn(); - s.ScaleTo(1); - double duration = RNG.Next(300, 1300); - s.FadeOutFromOne(duration, Easing.Out); - s.ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out); - - s.Expire(); + s.ScaleTo(1) + .ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out) + .FadeOutFromOne(duration, Easing.Out) + .Expire(); } })); } @@ -78,12 +91,12 @@ namespace osu.Game.Screens.Menu } }; - rotation = RNG.NextSingle(-2f, 2f); + rotation = getRandomVariance(2); } protected override void Update() { - const float gravity = 0.004f; + const float gravity = 0.003f; base.Update(); @@ -95,5 +108,7 @@ namespace osu.Game.Screens.Menu Rotation += rotation * elapsed; } } + + private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); } } From 4b06b6f34ec98b37bdd68e597a8248ac9c36df89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 18:53:32 +0200 Subject: [PATCH 236/555] Crudely fix crashes when switching between `TestResultsWithPlayer` cases In the visual test browser, if two `TestResultsWithPlayer` test cases are ran consecutively, the second would die on `SoloStatisticsWatcher` seeing duplicated online score IDs. This surfaced after 6ef39b87fed8b44b5f9219da12d639ae5e4f422b, which changed `TestResultsScreen` to inherit `SoloResultsScreen` rather than `ResultsScreen`. This is probably _not_ a very good fix, but I'm trying to be pragmatic for now. `SoloStatisticsWatcher` should probably not live in `OsuGameBase`. --- .../Visual/Ranking/TestSceneResultsScreen.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 1c904a936f..146482e6fb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -69,6 +69,8 @@ namespace osu.Game.Tests.Visual.Ranking })); } + private int onlineScoreID = 1; + [TestCase(1, ScoreRank.X)] [TestCase(0.9999, ScoreRank.S)] [TestCase(0.975, ScoreRank.S)] @@ -81,14 +83,17 @@ namespace osu.Game.Tests.Visual.Ranking { TestResultsScreen screen = null; - var score = TestResources.CreateTestScoreInfo(); + loadResultsScreen(() => + { + var score = TestResources.CreateTestScoreInfo(); - score.OnlineID = 1234; - score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); - score.Accuracy = accuracy; - score.Rank = rank; + score.OnlineID = onlineScoreID++; + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + score.Accuracy = accuracy; + score.Rank = rank; - loadResultsScreen(() => screen = createResultsScreen(score)); + return screen = createResultsScreen(score); + }); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay present", () => screen.RetryOverlay != null); } From 20bde40ce21b41d5a19add9e5a28e472f1460919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 20:00:25 +0200 Subject: [PATCH 237/555] Fix differing anchor specs on statistics panel flow items --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 9357d1f385..19bd0c4393 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -157,8 +157,8 @@ namespace osu.Game.Screens.Ranking.Statistics flow.Add(new StatisticItemContainer(item) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, }); } From 632077319f3e4763a6654a137c5723f5daba84c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 20:06:53 +0200 Subject: [PATCH 238/555] Fix taiko statistics table not using 2-column layout --- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index feb8b0c2c2..584a91bfdc 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) From 4114fab8ea908ca5cd93d28f1671a7c35deb2f8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 03:31:36 +0900 Subject: [PATCH 239/555] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 759167829c..411a27b0b0 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7968364243..f147dfb6c1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 2e691da079..71978e94fc 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From 4859b8c9947a3b6d9acb39767da6a84939ed0c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 20:49:34 +0200 Subject: [PATCH 240/555] Add some extra text coverage against potential emoji-to-link misparses --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 8 ++++++++ osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 1 + 2 files changed, 9 insertions(+) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 46dc47bf53..47f4410b07 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -510,6 +510,14 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(result.Links.Count, 0); } + [Test] + public void TestEmojiWithSuccessiveParens() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "\uD83D\uDE10(let's hope this doesn't accidentally turn into a link)" }); + Assert.AreEqual("[emoji](let's hope this doesn't accidentally turn into a link)", result.DisplayContent); + Assert.AreEqual(result.Links.Count, 0); + } + [Test] public void TestAbsoluteExternalLinks() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index f5cf4c1ff2..ecf9b68297 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -89,6 +89,7 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) { From ae7ed697ec6e49168b1f857cb16f885ce3699906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:22:48 +0200 Subject: [PATCH 241/555] Adjust failing test after permitting autoplay and no fail combo --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index bedb2ceaa1..88904bf85b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Navigation { advanceToSongSelect(); openSkinEditor(); - AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() }); + AddStep("select relax and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModRelax(), new OsuModSpunOut() }); switchToGameplayScene(); From a1e83b20e660f4c677859524a82ddff1d4977429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:23:57 +0200 Subject: [PATCH 242/555] Make autoplay compatible with `ModFailCondition` --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModFailCondition.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index a4ffbeacef..88b41924ff 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; - public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModAdaptiveSpeed) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 97789b7f5a..e671c065cf 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride { - public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax) }; [SettingSource("Restart on fail", "Automatically restarts when failed.")] public BindableBool Restart { get; } = new BindableBool(); From 674eb1a36b8bb211665616cba1ad233c84e24618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:25:43 +0200 Subject: [PATCH 243/555] Remove unused property --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 88b41924ff..a3a4adc53d 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; - public bool RestartOnFail => false; - public override bool UserPlayable => false; public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; From eeb50e27007dcf5076b4a0a99ad0e44d7226b8cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:45:06 +0200 Subject: [PATCH 244/555] Do not rely on `OverlayColourProvider` presence --- osu.Game/Overlays/RevertToDefaultButton.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 48491c5d9c..28433f1669 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -33,7 +34,10 @@ namespace osu.Game.Overlays private Circle circle = null!; [Resolved] - private OverlayColourProvider colours { get; set; } = null!; + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } public Bindable Current { @@ -124,16 +128,16 @@ namespace osu.Game.Overlays { icon.RotateTo(-40, 500, Easing.OutQuint); - icon.FadeColour(colours.Light1, 300, Easing.OutQuint); - circle.FadeColour(colours.Background2, 300, Easing.OutQuint); + icon.FadeColour(colourProvider?.Light1 ?? colours.YellowLight, 300, Easing.OutQuint); + circle.FadeColour(colourProvider?.Background2 ?? colours.Gray6, 300, Easing.OutQuint); this.ScaleTo(1.2f, 300, Easing.OutQuint); } else { icon.RotateTo(0, 100, Easing.OutQuint); - icon.FadeColour(colours.Colour0, 100, Easing.OutQuint); - circle.FadeColour(colours.Background3, 100, Easing.OutQuint); + icon.FadeColour(colourProvider?.Colour0 ?? colours.Yellow, 100, Easing.OutQuint); + circle.FadeColour(colourProvider?.Background3 ?? colours.Gray3, 100, Easing.OutQuint); this.ScaleTo(1f, 100, Easing.OutQuint); } } From 6b9dabbd3c776800f10e0ccad7253fda22edec5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:54:28 +0200 Subject: [PATCH 245/555] Rewrite test scene to show non-themed and themed variants --- .../TestSceneRevertToDefaultButton.cs | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index bfef120358..0e31757396 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -2,58 +2,49 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Overlays; +using osu.Game.Tests.Visual.UserInterface; using osuTK; namespace osu.Game.Tests.Visual.Settings { - public partial class TestSceneRevertToDefaultButton : OsuTestScene + public partial class TestSceneRevertToDefaultButton : ThemeComparisonTestScene { private float scale = 1; - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly Bindable current = new Bindable { Default = default, Value = 1, }; - [Test] - public void TestBasic() + protected override Drawable CreateContent() => new Container { - RevertToDefaultButton revertToDefaultButton = null!; - - AddStep("create button", () => Child = new Container + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new RevertToDefaultButton { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background2, - }, - revertToDefaultButton = new RevertToDefaultButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(scale), - Current = current, - } - } - }); + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(scale), + Current = current, + } + }; + + [Test] + public void TestStates() + { + CreateThemedContent(OverlayColourScheme.Purple); AddSliderStep("set scale", 1, 4, 1, scale => { this.scale = scale; - if (revertToDefaultButton != null) - revertToDefaultButton.Scale = new Vector2(scale); + foreach (var revertToDefaultButton in this.ChildrenOfType>()) + revertToDefaultButton.Parent!.Scale = new Vector2(scale); }); AddToggleStep("toggle default state", state => current.Value = state ? default : 1); AddToggleStep("toggle disabled state", state => current.Disabled = state); From 3ca767b7a26564e8ed07903a689cb9660585261f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 21:58:55 +0200 Subject: [PATCH 246/555] Remove outdated comment --- osu.Game/Overlays/RevertToDefaultButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 28433f1669..94d56971e0 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -64,7 +64,6 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - // size intentionally much larger than actual drawn content, so that the button is easier to click. Size = new Vector2(14); AddRange(new Drawable[] From 9cc2150b5a37dfb0ecf940f31489dbf20c157384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 22:00:21 +0200 Subject: [PATCH 247/555] Fix mixed `[cC]urrent` usage --- osu.Game/Overlays/RevertToDefaultButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 94d56971e0..c43d233fc1 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -121,7 +121,7 @@ namespace osu.Game.Overlays Enabled.Value = !current.Disabled; - this.FadeTo(current.Disabled ? 0.2f : (Current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); + this.FadeTo(current.Disabled ? 0.2f : (current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); if (IsHovered && Enabled.Value) { From e7dbe3c9380b664dc1a67ad8b36eeabd6cd4a131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jul 2023 22:28:26 +0200 Subject: [PATCH 248/555] Fix broken test --- .../Visual/Settings/TestSceneRevertToDefaultButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs index 0e31757396..d081f663ba 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestStates() { - CreateThemedContent(OverlayColourScheme.Purple); + AddStep("create content", () => CreateThemedContent(OverlayColourScheme.Purple)); AddSliderStep("set scale", 1, 4, 1, scale => { this.scale = scale; From dd8774a64006792556b357d78756438324a34b37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 13:21:28 +0900 Subject: [PATCH 249/555] Vertically centre the editor osu! playfield --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index aac5f6ffb1..8d93613156 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit PlayfieldContentContainer.Padding = new MarginPadding { Vertical = 10, - Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10, - Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10, + // Intentionally use the left toolbox size for both sides to vertically centre the playfield. + Horizontal = TOOLBOX_CONTRACTED_SIZE_LEFT + 10, }; LayerBelowRuleset.AddRange(new Drawable[] From 56acc9e3dde3f161c79f7fafc312d9800e0fc420 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:02:48 +0900 Subject: [PATCH 250/555] Change `BeatDivisorControl` to retrive bindable divisor via DI --- .../Visual/Editing/TestSceneBeatDivisorControl.cs | 9 +++++++-- .../Visual/Editing/TestSceneTimelineTickDisplay.cs | 2 +- .../Edit/Compose/Components/BeatDivisorControl.cs | 8 ++------ osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 10 ++-------- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index 88b959a2a0..f2b3351533 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -22,7 +22,6 @@ namespace osu.Game.Tests.Visual.Editing public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { private BeatDivisorControl beatDivisorControl = null!; - private BindableBeatDivisor bindableBeatDivisor = null!; private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single(); private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single(); @@ -30,13 +29,19 @@ namespace osu.Game.Tests.Visual.Editing [Cached] private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Cached] + private readonly BindableBeatDivisor bindableBeatDivisor = new BindableBeatDivisor(16); + [SetUp] public void SetUp() => Schedule(() => { + bindableBeatDivisor.ValidDivisors.SetDefault(); + bindableBeatDivisor.SetDefault(); + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16)) + Child = beatDivisorControl = new BeatDivisorControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs index 1376ba23fb..18bd6d840a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing { BeatDivisor.Value = 4; - Add(new BeatDivisorControl(BeatDivisor) + Add(new BeatDivisorControl { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index f7159f8670..59b0bd1785 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -33,12 +33,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { private int? lastCustomDivisor; - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - - public BeatDivisorControl(BindableBeatDivisor beatDivisor) - { - this.beatDivisor.BindTo(beatDivisor); - } + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index b8cbff047e..0c5f28c2d6 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -3,7 +3,6 @@ #nullable disable -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,8 +18,6 @@ namespace osu.Game.Screens.Edit { private const float padding = 10; - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private Container timelineContainer; protected EditorScreenWithTimeline(EditorScreenMode type) @@ -33,11 +30,8 @@ namespace osu.Game.Screens.Edit private LoadingSpinner spinner; [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider colourProvider, [CanBeNull] BindableBeatDivisor beatDivisor) + private void load(OverlayColourProvider colourProvider) { - if (beatDivisor != null) - this.beatDivisor.BindTo(beatDivisor); - Child = new GridContainer { RelativeSizeAxes = Axes.Both, @@ -82,7 +76,7 @@ namespace osu.Game.Screens.Edit AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Right = 5 }, }, - new BeatDivisorControl(this.beatDivisor) { RelativeSizeAxes = Axes.Both } + new BeatDivisorControl { RelativeSizeAxes = Axes.Both } }, }, RowDimensions = new[] From ebaf63b7644425dd06f9e1ba00e8159070813ccf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:04:12 +0900 Subject: [PATCH 251/555] Apply NRT to timeline related classes --- .../Edit/Compose/Components/Timeline/TimelineArea.cs | 8 +++----- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 12 +++++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 0b83258f8b..02c773fd4b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,16 +16,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineArea : CompositeDrawable { - public Timeline Timeline; + public Timeline Timeline = null!; private readonly Drawable userContent; - public TimelineArea(Drawable content = null) + public TimelineArea(Drawable? content = null) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - userContent = content ?? Drawable.Empty(); + userContent = content ?? Empty(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 0c5f28c2d6..bd57ea42f0 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,17 +16,17 @@ namespace osu.Game.Screens.Edit { private const float padding = 10; - private Container timelineContainer; + private Container timelineContainer = null!; + + private Container mainContent = null!; + + private LoadingSpinner spinner = null!; protected EditorScreenWithTimeline(EditorScreenMode type) : base(type) { } - private Container mainContent; - - private LoadingSpinner spinner; - [BackgroundDependencyLoader(true)] private void load(OverlayColourProvider colourProvider) { From 5b2e70426488a295df83f83345326d7181b83568 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:19:04 +0900 Subject: [PATCH 252/555] Move beat divisor control inside of `TimelineArea` and adjust metrics to match design --- .../Components/Timeline/TimelineArea.cs | 44 +++++++++---------- .../Screens/Edit/EditorScreenWithTimeline.cs | 5 ++- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 02c773fd4b..3969a002db 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -48,14 +48,24 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 140), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 110), + }, Content = new[] { new Drawable[] { new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Name = @"Toggle controls", Children = new Drawable[] { @@ -92,31 +102,31 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, + Timeline = new Timeline(userContent), new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Name = @"Zoom controls", + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, + Colour = colourProvider.Background2, }, new Container { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Masking = true, Children = new[] { new TimelineButton { - RelativeSizeAxes = Axes.Y, - Height = 0.5f, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), Icon = FontAwesome.Solid.SearchPlus, Action = () => Timeline.AdjustZoomRelatively(1) }, @@ -124,8 +134,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Y, - Height = 0.5f, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), Icon = FontAwesome.Solid.SearchMinus, Action = () => Timeline.AdjustZoomRelatively(-1) }, @@ -133,19 +143,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - Timeline = new Timeline(userContent), + new BeatDivisorControl { RelativeSizeAxes = Axes.Both } }, }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } } }; diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index bd57ea42f0..a2367dd66c 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit @@ -30,6 +29,9 @@ namespace osu.Game.Screens.Edit [BackgroundDependencyLoader(true)] private void load(OverlayColourProvider colourProvider) { + // Grid with only two rows. + // First is the timeline area, which should be allowed to expand as required. + // Second is the main editor content, including the playfield and side toolbars (but not the bottom). Child = new GridContainer { RelativeSizeAxes = Axes.Both, @@ -74,7 +76,6 @@ namespace osu.Game.Screens.Edit AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Right = 5 }, }, - new BeatDivisorControl { RelativeSizeAxes = Axes.Both } }, }, RowDimensions = new[] From 1dc293ed619adc285ac5554b4ea6a65e04049f77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:24:53 +0900 Subject: [PATCH 253/555] Allow specifying a custom width for nubs in `OsuCheckbox`es --- osu.Game/Graphics/UserInterface/Nub.cs | 6 +++--- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 8 ++++---- osu.Game/Graphics/UserInterface/RoundedSliderBar.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 28a2eb40c3..4b953718bc 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -20,16 +20,16 @@ namespace osu.Game.Graphics.UserInterface { public const float HEIGHT = 15; - public const float EXPANDED_SIZE = 50; + public const float DEFAULT_EXPANDED_SIZE = 50; private const float border_width = 3; private readonly Box fill; private readonly Container main; - public Nub() + public Nub(float expandedSize = DEFAULT_EXPANDED_SIZE) { - Size = new Vector2(EXPANDED_SIZE, HEIGHT); + Size = new Vector2(expandedSize, HEIGHT); InternalChildren = new[] { diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 160105af1a..b7b405a7e8 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -47,7 +47,7 @@ namespace osu.Game.Graphics.UserInterface private Sample sampleChecked; private Sample sampleUnchecked; - public OsuCheckbox(bool nubOnRight = true) + public OsuCheckbox(bool nubOnRight = true, float nubSize = Nub.DEFAULT_EXPANDED_SIZE) { AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -61,7 +61,7 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }, - Nub = new Nub(), + Nub = new Nub(nubSize), new HoverSounds() }; @@ -70,14 +70,14 @@ namespace osu.Game.Graphics.UserInterface Nub.Anchor = Anchor.CentreRight; Nub.Origin = Anchor.CentreRight; Nub.Margin = new MarginPadding { Right = nub_padding }; - LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; } else { Nub.Anchor = Anchor.CentreLeft; Nub.Origin = Anchor.CentreLeft; Nub.Margin = new MarginPadding { Left = nub_padding }; - LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; } Nub.Current.BindTo(Current); diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index a666b83c05..56af23ff10 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -51,7 +51,7 @@ namespace osu.Game.Graphics.UserInterface public RoundedSliderBar() { Height = Nub.HEIGHT; - RangePadding = Nub.EXPANDED_SIZE / 2; + RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2; Children = new Drawable[] { new Container From 01750dd0914e13b6ce4ee1dda8c878639b7d9972 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:24:40 +0900 Subject: [PATCH 254/555] Update metrics of checkboxes and backgrounds to match design better --- .../Components/Timeline/TimelineArea.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 3969a002db..70322bd3e0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -39,11 +39,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline InternalChildren = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5 - }, new GridContainer { RelativeSizeAxes = Axes.X, @@ -76,24 +71,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new FillFlowContainer { - AutoSizeAxes = Axes.Y, - Width = 160, + RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(10), Direction = FillDirection.Vertical, Spacing = new Vector2(0, 4), Children = new[] { - waveformCheckbox = new OsuCheckbox + waveformCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = EditorStrings.TimelineWaveform, Current = { Value = true }, }, - ticksCheckbox = new OsuCheckbox + ticksCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = EditorStrings.TimelineTicks, Current = { Value = true }, }, - controlPointsCheckbox = new OsuCheckbox + controlPointsCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = BeatmapsetsStrings.ShowStatsBpm, Current = { Value = true }, @@ -102,7 +96,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - Timeline = new Timeline(userContent), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Colour = colourProvider.Background5 + }, + Timeline = new Timeline(userContent), + } + }, new Container { RelativeSizeAxes = Axes.Both, From 6b222cfafdedf6bb7299e189f30b533aa83b659f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 14:29:13 +0900 Subject: [PATCH 255/555] Fix slight misalignment so timeline is now completely centered --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index a2367dd66c..104d9dd58f 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit { public abstract partial class EditorScreenWithTimeline : EditorScreen { - private const float padding = 10; + private const float padding = 15; private Container timelineContainer = null!; @@ -74,7 +74,6 @@ namespace osu.Game.Screens.Edit { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 5 }, }, }, }, From 00e9746174b842bcec7cb6ea2c08bd2e55932580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 15:17:24 +0900 Subject: [PATCH 256/555] Implement longer design for timing point piece --- .../Timeline/TimelineControlPointGroup.cs | 2 +- .../Components/Timeline/TopPointPiece.cs | 64 ++++++++++++++----- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index fc3ef92bf5..c1b6069523 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; - Origin = Anchor.TopCentre; + Origin = Anchor.TopLeft; X = (float)group.Time; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs index 69fb001a66..243cdc6ddd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -10,24 +8,25 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TopPointPiece : CompositeDrawable { - private readonly ControlPoint point; + protected readonly ControlPoint Point; - protected OsuSpriteText Label { get; private set; } + protected OsuSpriteText Label { get; private set; } = null!; + + private const float width = 80; public TopPointPiece(ControlPoint point) { - this.point = point; - AutoSizeAxes = Axes.X; + Point = point; + Width = width; Height = 16; - Margin = new MarginPadding(4); - - Masking = true; - CornerRadius = Height / 2; + Margin = new MarginPadding { Vertical = 4 }; Origin = Anchor.TopCentre; Anchor = Anchor.TopCentre; @@ -36,17 +35,52 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load(OsuColour colours) { + const float corner_radius = 4; + const float arrow_extension = 3; + const float triangle_portion = 15; + InternalChildren = new Drawable[] { - new Box + // This is a triangle, trust me. + // Doing it this way looks okay. Doing it using Triangle primitive is basically impossible. + new Container { - Colour = point.GetRepresentingColour(colours), - RelativeSizeAxes = Axes.Both, + Colour = Point.GetRepresentingColour(colours), + X = -corner_radius, + Size = new Vector2(triangle_portion * arrow_extension, Height), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Masking = true, + CornerRadius = Height, + CornerExponent = 1.4f, + Children = new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Y, + Width = width - triangle_portion, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Point.GetRepresentingColour(colours), + Masking = true, + CornerRadius = corner_radius, + Child = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, }, Label = new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Padding = new MarginPadding(3), Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold), Colour = colours.B5, From 57abb15724e537bee9a97ffdbd810b632b675671 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 15:58:53 +0900 Subject: [PATCH 257/555] Update design of timeline centre marker and adjust surrounding paddings --- .../Components/Timeline/CentreMarker.cs | 20 +++++++------------ .../Components/Timeline/TimelineArea.cs | 20 +++++++++++++------ .../Screens/Edit/EditorScreenWithTimeline.cs | 4 ++-- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index aee3cffbfd..74786cc0c9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -12,17 +12,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class CentreMarker : CompositeDrawable { - private const float triangle_width = 15; - private const float triangle_height = 10; - private const float bar_width = 2; + private const float triangle_width = 8; + + private const float bar_width = 1.6f; public CentreMarker() { RelativeSizeAxes = Axes.Y; Size = new Vector2(triangle_width, 1); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; InternalChildren = new Drawable[] { @@ -37,22 +37,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, - Size = new Vector2(triangle_width, triangle_height), + Size = new Vector2(triangle_width, triangle_width * 0.8f), Scale = new Vector2(1, -1) }, - new Triangle - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(triangle_width, triangle_height), - } }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - Colour = colours.RedDark; + Colour = colours.Red1; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 70322bd3e0..fb7ce8e423 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays; @@ -29,10 +30,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Masking = true; - OsuCheckbox waveformCheckbox; OsuCheckbox controlPointsCheckbox; OsuCheckbox ticksCheckbox; @@ -51,7 +50,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { new Dimension(GridSizeMode.Absolute, 140), new Dimension(), - new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 35), new Dimension(GridSizeMode.Absolute, 110), }, Content = new[] @@ -102,6 +101,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AutoSizeAxes = Axes.Y, Children = new Drawable[] { + // the out-of-bounds portion of the centre marker. + new Box + { + Width = 24, + Height = EditorScreenWithTimeline.PADDING, + Depth = float.MaxValue, + Colour = colours.Red1, + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, new Box { RelativeSizeAxes = Axes.Both, @@ -115,7 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.Both, Name = @"Zoom controls", - Padding = new MarginPadding { Right = 5 }, + Padding = new MarginPadding { Right = 10 }, Children = new Drawable[] { new Box @@ -128,7 +137,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Masking = true, Children = new[] { new TimelineButton diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 104d9dd58f..ea2790b50a 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit { public abstract partial class EditorScreenWithTimeline : EditorScreen { - private const float padding = 15; + public const float PADDING = 10; private Container timelineContainer = null!; @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Edit Name = "Timeline content", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = padding, Top = padding }, + Padding = new MarginPadding { Horizontal = PADDING, Top = PADDING }, Child = new GridContainer { RelativeSizeAxes = Axes.X, From e6b8cd0c06abce3e02806c570b4316a484c2d817 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 16:55:12 +0900 Subject: [PATCH 258/555] Add editor header --- .../Edit/Components/Menus/EditorMenuBar.cs | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index a911b4e1d8..fa4e52d6a6 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -3,6 +3,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -14,19 +17,56 @@ namespace osu.Game.Screens.Edit.Components.Menus { public partial class EditorMenuBar : OsuMenu { + private const float heading_area = 114; + public EditorMenuBar() : base(Direction.Horizontal, true) { RelativeSizeAxes = Axes.X; MaskingContainer.CornerRadius = 0; - ItemsContainer.Padding = new MarginPadding { Left = 100 }; + ItemsContainer.Padding = new MarginPadding { Left = heading_area }; } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, TextureStore textures) { BackgroundColour = colourProvider.Background3; + + TextFlowContainer text; + + AddRangeInternal(new[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = heading_area, + Padding = new MarginPadding(8), + Children = new Drawable[] + { + new Sprite + { + Size = new Vector2(26), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Texture = textures.Get("Icons/Hexacons/editor"), + }, + text = new TextFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + } + } + }, + }); + + text.AddText("osu!", t => t.Font = OsuFont.TorusAlternate); + text.AddText("editor", t => + { + t.Font = OsuFont.TorusAlternate; + t.Colour = colourProvider.Highlight1; + }); } protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); From fe70f2492538533148fd3ee28a2749901736bf43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 17:00:42 +0900 Subject: [PATCH 259/555] Update design of summary timeline current time marker --- .../Timelines/Summary/Parts/MarkerPart.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index d42c02e03d..ff707407dd 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -73,6 +73,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public MarkerVisualisation() { + const float box_height = 4; + Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; RelativePositionAxes = Axes.X; @@ -80,32 +82,46 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts AutoSizeAxes = Axes.X; InternalChildren = new Drawable[] { + new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(14, box_height), + }, new Triangle { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, Scale = new Vector2(1, -1), Size = new Vector2(10, 5), + Y = box_height, }, new Triangle { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Size = new Vector2(10, 5) + Size = new Vector2(10, 5), + Y = -box_height, + }, + new Box + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(14, box_height), }, new Box { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - Width = 2, + Width = 1.4f, EdgeSmoothness = new Vector2(1, 0) } }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Red; + private void load(OsuColour colours) => Colour = colours.Red1; } } } From 85c780ae5be25c3b89ea414a2b03d688cefdd452 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 19:19:14 +0900 Subject: [PATCH 260/555] Allow the osu! logo to be proxied locally into scenes --- osu.Game/OsuGame.cs | 4 +-- osu.Game/Screens/Menu/MainMenu.cs | 7 ++++++ osu.Game/Screens/Menu/OsuLogo.cs | 41 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5b654e0c16..1a40bb8e3d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -950,9 +950,9 @@ namespace osu.Game if (!args?.Any(a => a == @"--no-version-overlay") ?? true) loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add); - loadComponentSingleFile(osuLogo, logo => + loadComponentSingleFile(osuLogo, _ => { - logoContainer.Add(logo); + osuLogo.SetupDefaultContainer(logoContainer); // Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering. ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index baa34d4914..510c9a5373 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -85,6 +86,7 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; + private Container logoTarget; [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) @@ -129,6 +131,7 @@ namespace osu.Game.Screens.Menu } } }, + logoTarget = new Container { RelativeSizeAxes = Axes.Both, }, sideFlashes = new MenuSideFlashes(), songTicker = new SongTicker { @@ -208,6 +211,8 @@ namespace osu.Game.Screens.Menu logo.FadeColour(Color4.White, 100, Easing.OutQuint); logo.FadeIn(100, Easing.OutQuint); + logo.ProxyToContainer(logoTarget); + if (resuming) { Buttons.State = ButtonSystemState.TopLevel; @@ -245,6 +250,8 @@ namespace osu.Game.Screens.Menu var seq = logo.FadeOut(300, Easing.InSine) .ScaleTo(0.2f, 300, Easing.InSine); + logo.ReturnProxy(); + seq.OnComplete(_ => Buttons.SetOsuLogo(null)); seq.OnAbort(_ => Buttons.SetOsuLogo(null)); } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 277b8bf888..8867ecfb2a 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -435,5 +435,46 @@ namespace osu.Game.Screens.Menu logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic); base.OnDragEnd(e); } + + private Container defaultProxyTarget; + private Container currentProxyTarget; + private Drawable proxy; + + public Drawable ProxyToContainer(Container c) + { + if (currentProxyTarget != null) + throw new InvalidOperationException("Previous proxy usage was not returned"); + + if (defaultProxyTarget == null) + throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first"); + + currentProxyTarget = c; + + defaultProxyTarget.Remove(proxy, false); + currentProxyTarget.Add(proxy); + return proxy; + } + + public void ReturnProxy() + { + if (currentProxyTarget == null) + throw new InvalidOperationException("No usage to return"); + + if (defaultProxyTarget == null) + throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first"); + + currentProxyTarget.Remove(proxy, false); + currentProxyTarget = null; + + defaultProxyTarget.Add(proxy); + } + + public void SetupDefaultContainer(Container container) + { + defaultProxyTarget = container; + + defaultProxyTarget.Add(this); + defaultProxyTarget.Add(proxy = CreateProxy()); + } } } From d4fb0bef95cd9ede8cb8da9df98dfc07c7791833 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 19:29:36 +0900 Subject: [PATCH 261/555] Fix osu!mania scores failing to convert to new standardised score due to cast failure Regressed in https://github.com/ppy/osu/pull/23917. Closes #24217. --- .../Scoring/ManiaScoreProcessor.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index a0f6ac572d..16e10006e3 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Scoring } protected override IEnumerable EnumerateHitObjects(IBeatmap beatmap) - => base.EnumerateHitObjects(beatmap).OrderBy(ho => (ManiaHitObject)ho, JudgementOrderComparer.DEFAULT); + => base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT); protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { @@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)); - private class JudgementOrderComparer : IComparer + private class JudgementOrderComparer : IComparer { public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer(); - public int Compare(ManiaHitObject? x, ManiaHitObject? y) + public int Compare(HitObject? x, HitObject? y) { if (ReferenceEquals(x, y)) return 0; if (ReferenceEquals(x, null)) return -1; @@ -48,11 +48,14 @@ namespace osu.Game.Rulesets.Mania.Scoring if (result != 0) return result; - // due to the way input is handled in mania, notes take precedence over ticks in judging order. - if (x is Note && y is not Note) return -1; - if (x is not Note && y is Note) return 1; + var xNote = x as Note; + var yNote = y as Note; - return x.Column.CompareTo(y.Column); + // due to the way input is handled in mania, notes take precedence over ticks in judging order. + if (xNote != null && yNote == null) return -1; + if (xNote == null && yNote != null) return 1; + + return xNote!.Column.CompareTo(yNote!.Column); } } } From 480259b8d2b4cfc2f2e06e13eaab6ea1f2162dd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jul 2023 20:02:25 +0900 Subject: [PATCH 262/555] Ensure migration is never run on scores with new-enough `TotalScoreVersion`s --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index b8afdad294..11a5e252a3 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -26,6 +26,9 @@ namespace osu.Game.Database if (score.IsLegacyScore) return false; + if (score.TotalScoreVersion > 30000002) + return false; + // Recalculate the old-style standardised score to see if this was an old lazer score. bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore; // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. From 1868826d692f9d0c0fc13a9184758a1c3518a85f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jul 2023 01:12:10 +0900 Subject: [PATCH 263/555] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 411a27b0b0..49b9de678d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0ccf851138..f56133e64c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 71978e94fc..12f84c66c2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From e58488d04a64acb9a08cee6253da5973d2c2c9bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jul 2023 11:19:34 +0900 Subject: [PATCH 264/555] Fix incorrect comparer implementation --- .../Scoring/ManiaScoreProcessor.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 16e10006e3..c53f3c3e07 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -48,14 +48,13 @@ namespace osu.Game.Rulesets.Mania.Scoring if (result != 0) return result; - var xNote = x as Note; - var yNote = y as Note; - // due to the way input is handled in mania, notes take precedence over ticks in judging order. - if (xNote != null && yNote == null) return -1; - if (xNote == null && yNote != null) return 1; + if (x is Note && y is not Note) return -1; + if (x is not Note && y is Note) return 1; - return xNote!.Column.CompareTo(yNote!.Column); + return x is ManiaHitObject maniaX && y is ManiaHitObject maniaY + ? maniaX.Column.CompareTo(maniaY.Column) + : 0; } } } From eb81eac635401bde3c923f03a1a7238c9b03caf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jul 2023 12:19:18 +0900 Subject: [PATCH 265/555] Flag decoded scores more correctly --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c6461840aa..8c00110909 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -49,6 +49,13 @@ namespace osu.Game.Scoring.Legacy scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION; + // TotalScoreVersion gets initialised to LATEST_VERSION. + // In the case where the incoming score has either an osu!stable or old lazer version, we need + // to mark it with the correct version increment to trigger reprocessing to new standardised scoring. + // + // See StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(). + scoreInfo.TotalScoreVersion = version < 30000002 ? 30000001 : LegacyScoreEncoder.LATEST_VERSION; + string beatmapHash = sr.ReadString(); workingBeatmap = GetBeatmap(beatmapHash); From 7f957d3fbef45d745d9ebc545eefa6050fa3b84f Mon Sep 17 00:00:00 2001 From: chayleaf Date: Sat, 15 Jul 2023 20:24:40 +0700 Subject: [PATCH 266/555] Fix some taiko maps not finishing in some conditions I don't know how to reproduce this issue in a test, so no tests for now. Nonetheless, this fixes the issue for me at least on one map: https://osu.ppy.sh/beatmapsets/1899665#taiko/3915653 This workaround is similar to #16475 (the test from that commit got eventually removed for some reason). --- .../Objects/Drawables/DrawableStrongNestedHit.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 9b410d1871..eb31708e20 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -16,5 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables : base(nestedHit) { } + + public override void OnKilled() + { + base.OnKilled(); + + if (!Judged) + ApplyResult(r => r.Type = r.Judgement.MinResult); + } } } From 309c8522227d63d647847ad0cf90ccf1f5ffd6eb Mon Sep 17 00:00:00 2001 From: Aki <75532970+AkiSakurai@users.noreply.github.com> Date: Sat, 15 Jul 2023 22:12:48 +0800 Subject: [PATCH 267/555] Compute the top local rank directly without an expensive detach call --- osu.Game/Scoring/ScoreInfoExtensions.cs | 8 ++++++++ osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 6e57a9fd0b..63cc077cde 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -32,5 +32,13 @@ namespace osu.Game.Scoring /// The to compute the maximum achievable combo for. /// The maximum achievable combo. public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); + + /// + /// Retrieves the with the maximum total score. + /// + /// An array of s to retrieve the scoreInfo with maximum total score. + /// The instance with the maximum total score. + public static ScoreInfo? MaxByTopScore(this IEnumerable scores) + => scores.MaxBy(info => (info.TotalScore, -info.OnlineID, -info.Date.UtcDateTime.Ticks)); } } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index c17de77619..fe2d79b080 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -75,7 +74,7 @@ namespace osu.Game.Screens.Select.Carousel if (changes?.HasCollectionChanges() == false) return; - ScoreInfo? topScore = sender.Detach().OrderByTotalScore().FirstOrDefault(); + ScoreInfo? topScore = sender.MaxByTopScore(); updateable.Rank = topScore?.Rank; updateable.Alpha = topScore != null ? 1 : 0; From 3e91d308254d63125e47a3a92f6d52bc9c2d551e Mon Sep 17 00:00:00 2001 From: chayleaf Date: Sat, 15 Jul 2023 22:33:16 +0700 Subject: [PATCH 268/555] move StrongNestedHit OnKilled to DrawableStrongNestedHit --- .../Objects/Drawables/DrawableDrumRoll.cs | 8 -------- .../Objects/Drawables/DrawableDrumRollTick.cs | 8 -------- .../Objects/Drawables/DrawableStrongNestedHit.cs | 3 ++- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 005d2ab1ac..2bf0c04adf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -195,14 +195,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public override void OnKilled() - { - base.OnKilled(); - - if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged) - ApplyResult(r => r.Type = r.Judgement.MinResult); - } - public override bool OnPressed(KeyBindingPressEvent e) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index abecd19545..c900165d34 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -108,14 +108,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public override void OnKilled() - { - base.OnKilled(); - - if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged) - ApplyResult(r => r.Type = r.Judgement.MinResult); - } - public override bool OnPressed(KeyBindingPressEvent e) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index eb31708e20..4d430987b9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -1,6 +1,7 @@ // 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.Objects; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.OnKilled(); - if (!Judged) + if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) ApplyResult(r => r.Type = r.Judgement.MinResult); } } From 542916f8570eb757870ab3d612edff9612c1bfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:15:42 +0200 Subject: [PATCH 269/555] Bring back test coverage for fail case from #16475 It was inadvertently dropped during refactoring in b185194d07f0ff1e6ebe7eafb796a85491fab118. --- .../Judgements/TestSceneDrumRollJudgements.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs index 21f2b8f1be..2f9f5e0a37 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs @@ -75,6 +75,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.IgnoreHit); } + [Test] + public void TestHitNoneStrongDrumRoll() + { + PerformTest(new List + { + new TaikoReplayFrame(0), + }, CreateBeatmap(createDrumRoll(true))); + + AssertJudgementCount(12); + + for (int i = 0; i < 5; ++i) + { + AssertResult(i, HitResult.IgnoreMiss); + AssertResult(i, HitResult.IgnoreMiss); + } + + AssertResult(0, HitResult.IgnoreHit); + } + [Test] public void TestHitAllStrongDrumRollWithOneKey() { From 24d63a4d96c99dc0ff55e50929a3f7b96166e5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:17:45 +0200 Subject: [PATCH 270/555] Add test coverage for failing hit judgement with HD active --- .../Judgements/JudgementTest.cs | 5 +++- .../Judgements/TestSceneHitJudgements.cs | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs index eb2d96ec51..f3e37736b2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs @@ -1,6 +1,7 @@ // 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 NUnit.Framework; @@ -10,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; @@ -36,11 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements () => Is.EqualTo(expectedResult)); } - protected void PerformTest(List frames, Beatmap? beatmap = null) + protected void PerformTest(List frames, Beatmap? beatmap = null, Mod[]? mods = null) { AddStep("load player", () => { Beatmap.Value = CreateWorkingBeatmap(beatmap); + SelectedMods.Value = mods ?? Array.Empty(); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 3bf94eb62e..b9e767e625 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -6,8 +6,10 @@ using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Tests.Judgements { @@ -157,5 +159,31 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertJudgementCount(1); AssertResult(0, HitResult.Ok); } + + [Test] + public void TestStrongHitWithHidden() + { + const double hit_time = 1000; + + var beatmap = CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + }); + + var hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), + }, beatmap, new[] { new TaikoModHidden() }); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Ok); + AssertResult(0, HitResult.IgnoreMiss); + } } } From 9e960894c2b03029a0d40d20a7c0cf9ef451ce5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:22:04 +0200 Subject: [PATCH 271/555] Add inline commentary about `OnKilled()` override --- .../Objects/Drawables/DrawableStrongNestedHit.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 4d430987b9..724d59edcd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -22,6 +22,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.OnKilled(); + // usually, the strong nested hit isn't judged itself, it is judged by its parent object. + // however, in rare cases (see: drum rolls, hits with hidden active), + // it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object. + // this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing. if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) ApplyResult(r => r.Type = r.Judgement.MinResult); } From 955aa70e46835d6200a4fabb5ba18d5bda1b5787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:43:31 +0200 Subject: [PATCH 272/555] Add failing test case for hitting nested hit past parent end time --- .../Judgements/TestSceneHitJudgements.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index b9e767e625..5e9d4bcf14 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Rulesets.Taiko.Scoring; @@ -161,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements } [Test] - public void TestStrongHitWithHidden() + public void TestStrongHitOneKeyWithHidden() { const double hit_time = 1000; @@ -185,5 +186,32 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.Ok); AssertResult(0, HitResult.IgnoreMiss); } + + [Test] + public void TestStrongHitTwoKeysWithHidden() + { + const double hit_time = 1000; + + var beatmap = CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + }); + + var hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre), + }, beatmap, new[] { new TaikoModHidden() }); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Ok); + AssertResult(0, HitResult.LargeBonus); + } } } From 041e81889209c0a568a42b38227b2cedf9c2d3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jul 2023 18:44:47 +0200 Subject: [PATCH 273/555] Fix nested hits not being hittable if cut off by parent object ending --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 4708ef9bf0..2c3b4a8d18 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) : hitObject.HitStateUpdateTime; + // extend the lifetime end of the object in order to allow its nested strong hit (if any) to be judged. + hitObject.LifetimeEnd += DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW; } break; From cb354685ca323f4e429e60617077ed5a5175ee3d Mon Sep 17 00:00:00 2001 From: Aki <75532970+AkiSakurai@users.noreply.github.com> Date: Sun, 16 Jul 2023 10:21:32 +0800 Subject: [PATCH 274/555] simplify code --- osu.Game/Scoring/ScoreInfoExtensions.cs | 8 -------- osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 63cc077cde..6e57a9fd0b 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -32,13 +32,5 @@ namespace osu.Game.Scoring /// The to compute the maximum achievable combo for. /// The maximum achievable combo. public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); - - /// - /// Retrieves the with the maximum total score. - /// - /// An array of s to retrieve the scoreInfo with maximum total score. - /// The instance with the maximum total score. - public static ScoreInfo? MaxByTopScore(this IEnumerable scores) - => scores.MaxBy(info => (info.TotalScore, -info.OnlineID, -info.Date.UtcDateTime.Ticks)); } } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index fe2d79b080..da9661f702 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -74,8 +75,7 @@ namespace osu.Game.Screens.Select.Carousel if (changes?.HasCollectionChanges() == false) return; - ScoreInfo? topScore = sender.MaxByTopScore(); - + ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks)); updateable.Rank = topScore?.Rank; updateable.Alpha = topScore != null ? 1 : 0; } From 416ee0d99cb0ddb344ec6183fb15278f45295b2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Jul 2023 12:48:58 +0900 Subject: [PATCH 275/555] Fix covariant array spec in new test --- .../Judgements/TestSceneHitJudgements.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 5e9d4bcf14..32966905c9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Mods; @@ -180,7 +181,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements { new TaikoReplayFrame(0), new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), - }, beatmap, new[] { new TaikoModHidden() }); + }, beatmap, new Mod[] { new TaikoModHidden() }); AssertJudgementCount(2); AssertResult(0, HitResult.Ok); @@ -207,7 +208,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements new TaikoReplayFrame(0), new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre), - }, beatmap, new[] { new TaikoModHidden() }); + }, beatmap, new Mod[] { new TaikoModHidden() }); AssertJudgementCount(2); AssertResult(0, HitResult.Ok); From ce12afde70e437fe1e03ebee24d70258a1dd3063 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 15 Jul 2023 23:38:06 -0700 Subject: [PATCH 276/555] Remove android manifest overlay --- osu.Android.props | 4 ---- osu.Android/Properties/AndroidManifestOverlay.xml | 15 --------------- 2 files changed, 19 deletions(-) delete mode 100644 osu.Android/Properties/AndroidManifestOverlay.xml diff --git a/osu.Android.props b/osu.Android.props index 49b9de678d..f5d986a458 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -8,14 +8,10 @@ true true - manifestmerger.jar - - - diff --git a/osu.Android/Properties/AndroidManifestOverlay.xml b/osu.Android/Properties/AndroidManifestOverlay.xml deleted file mode 100644 index 815f935383..0000000000 --- a/osu.Android/Properties/AndroidManifestOverlay.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file From 72bf43e297896f6ad4cd76644dc232269a435e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 16:28:44 +0200 Subject: [PATCH 277/555] Add failing test covering exit dialog crash --- .../Navigation/TestSceneScreenNavigation.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index c3b668f591..8e335b2345 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -722,6 +722,33 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); } + [Test] + public void TestForceExitWithOperationInProgress() + { + AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + + ProgressNotification progressNotification = null!; + + AddStep("start ongoing operation", () => + { + progressNotification = new ProgressNotification + { + Text = "Something is still running", + Progress = 0.5f, + State = ProgressNotificationState.Active, + }; + Game.Notifications.Post(progressNotification); + }); + + AddStep("attempt exit", () => + { + for (int i = 0; i < 2; ++i) + Game.ScreenStack.CurrentScreen.Exit(); + }); + AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); + } + [Test] public void TestExitGameFromSongSelect() { From d25a03984beba1ba009f4120920ced2820e3e122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 16:29:27 +0200 Subject: [PATCH 278/555] Fix `PerformAction()` potentially crashing when no matching button is found --- osu.Game/Overlays/Dialog/PopupDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index e1e5604e4c..d7316305cc 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -229,7 +229,7 @@ namespace osu.Game.Overlays.Dialog { // Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure // they are ready to be pressed. - Schedule(() => Buttons.OfType().First().TriggerClick()); + Schedule(() => Buttons.OfType().FirstOrDefault()?.TriggerClick()); } protected override bool OnKeyDown(KeyDownEvent e) From a42992d3fdc4a8ea4762a9f546282aea66e5eb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 17:10:26 +0200 Subject: [PATCH 279/555] Remove unused local variable --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8e335b2345..3acc2f0384 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -728,17 +728,14 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); - ProgressNotification progressNotification = null!; - AddStep("start ongoing operation", () => { - progressNotification = new ProgressNotification + Game.Notifications.Post(new ProgressNotification { Text = "Something is still running", Progress = 0.5f, State = ProgressNotificationState.Active, - }; - Game.Notifications.Post(progressNotification); + }); }); AddStep("attempt exit", () => From e25cd03e4bc2d831086edfcf863fef34a9a101c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jul 2023 00:55:25 +0900 Subject: [PATCH 280/555] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 49b9de678d..7200f45277 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f56133e64c..16c8160fb6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 12f84c66c2..01ab409fe4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - + From a6b1559e1f414a608be1f315bf8527877c219e3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jul 2023 02:24:51 +0900 Subject: [PATCH 281/555] Update `DiscordRichPresence` to pull in performance fix See https://github.com/Lachee/discord-rpc-csharp/pull/237 --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 16d6a81d40..25f4cff00e 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From cd02a8a9ca599b26304f3ef7b39796bbf2c8a68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 19:43:37 +0200 Subject: [PATCH 282/555] Fix `PopupDialog` potentially accumulating schedules during load --- osu.Game/Overlays/Dialog/PopupDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index d7316305cc..2e9087fdbd 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -229,7 +229,7 @@ namespace osu.Game.Overlays.Dialog { // Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure // they are ready to be pressed. - Schedule(() => Buttons.OfType().FirstOrDefault()?.TriggerClick()); + Scheduler.AddOnce(() => Buttons.OfType().FirstOrDefault()?.TriggerClick()); } protected override bool OnKeyDown(KeyDownEvent e) From 7fbd47e9eec893409211a4a1f49e9819f0ded742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Jul 2023 19:56:22 +0200 Subject: [PATCH 283/555] Fix `MultiplayerMatchSubScreen` erroneously pushing exit dialog on API failure If `IAPIProvider.State` changes from `Online` at any point when being on an `OnlinePlayScreen`, it will be forcefully exited from. However, `MultiplayerMatchSubScreen` had local logic that suppressed the exit in order to show a confirmation dialog. The problem is, that in the suppression logic, `MultiplayerMatchSubScreen` was checking `MultiplayerClient.IsConnected`, which is a SignalR flag, and was not checking `IAPIAccess.State`, which is maintained separately. Due to differing timeouts and failure thresholds, it is not impossible to have `MultiplayerClient.IsConnected == true` but `IAPIAccess.State != APIState.Online`. In such a case, the match subscreen would wrongly consider itself to be still online and due to that, push useless confirmation dialogs, while being in the process of being forcefully exited. This then caused the dialog to cause a crash, as it was calling `.Exit()` on the screen which would already have been exited by that point, by the force-exit flow. --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ecf38a956d..01f04b44c9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -49,6 +50,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -251,10 +255,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override bool OnExiting(ScreenExitEvent e) { // the room may not be left immediately after a disconnection due to async flow, - // so checking the IsConnected status is also required. - if (client.Room == null || !client.IsConnected.Value) + // so checking the MultiplayerClient / IAPIAccess statuses is also required. + if (client.Room == null || !client.IsConnected.Value || api.State.Value != APIState.Online) { - // room has not been created yet; exit immediately. + // room has not been created yet or we're offline; exit immediately. return base.OnExiting(e); } From 6200e207d292357656ceb4ad29bca093bcad9da1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 16 Jul 2023 15:21:15 -0400 Subject: [PATCH 284/555] use fa_download for updates instead of fa_upload --- osu.Game/Updater/NoActionUpdateManager.cs | 4 ++-- osu.Game/Updater/SimpleUpdateManager.cs | 4 ++-- osu.Game/Updater/UpdateManager.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 97d3275757..f776cd67be 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -47,7 +47,7 @@ namespace osu.Game.Updater { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Check with your package manager / provider to bring osu! up-to-date!", - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, }); return true; diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 1ecb73a154..bc1b0919b8 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -54,7 +54,7 @@ namespace osu.Game.Updater { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Click here to download the new version, which can be installed over the top of your existing installation", - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, Activated = () => { host.OpenUrlExternally(getBestUrl(latest)); diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 47c2a169ed..190748137a 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -134,7 +134,7 @@ namespace osu.Game.Updater { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, Size = new Vector2(34), Colour = OsuColour.Gray(0.2f), Depth = float.MaxValue, From 3888471148e0db66654e7ce4b34886671abe2828 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Sun, 16 Jul 2023 23:03:21 +0300 Subject: [PATCH 285/555] Add break length and bounds checks --- .../Editing/Checks/CheckBreaksTest.cs | 10 +++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 3 + osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 84 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs new file mode 100644 index 0000000000..664f72c5f8 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Tests.Editing.Checks +{ + public class CheckBreaksTest + { + + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 3988f29e13..4bcf74db45 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Edit // Timing new CheckPreviewTime(), + + // Events + new CheckBreaks() }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs new file mode 100644 index 0000000000..12dc5554f4 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -0,0 +1,84 @@ +// 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.Timing; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckBreaks : ICheck + { + // Breaks may be off by 1 ms. + private const int leniency_threshold = 1; + private const double min_start_threshold = 200; + + // Break end time depends on the upcoming object's pre-empt time. + // As things stand, "pre-empt time" is only defined for osu! standard + // This is a generic value representing AR=10 + // Relevant: https://github.com/ppy/osu/issues/14330#issuecomment-1002158551 + private const double min_end_threshold = 450; + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Events, "Breaks not achievable using the editor"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateEarlyStart(this), + new IssueTemplateLateEnd(this), + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + foreach (var breakPeriod in context.Beatmap.Breaks) + { + if (breakPeriod.Duration < BreakPeriod.MIN_BREAK_DURATION) + yield return new IssueTemplateTooShort(this).Create(breakPeriod.StartTime); + } + + foreach (var hitObject in context.Beatmap.HitObjects) + { + foreach (var breakPeriod in context.Beatmap.Breaks) + { + double diffStart = breakPeriod.StartTime - hitObject.GetEndTime(); + double diffEnd = hitObject.StartTime - breakPeriod.EndTime; + + if (diffStart < min_start_threshold - leniency_threshold && diffStart > 0) + yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, min_start_threshold - diffStart); + else if (diffEnd < min_end_threshold - leniency_threshold && diffEnd > 0) + yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - diffEnd); + } + } + } + + public class IssueTemplateEarlyStart : IssueTemplate + { + public IssueTemplateEarlyStart(ICheck check) + : base(check, IssueType.Problem, "Break starts {0} ms early.") + { + } + + public Issue Create(double startTime, double diff) => new Issue(startTime, this, (int)diff); + } + + public class IssueTemplateLateEnd : IssueTemplate + { + public IssueTemplateLateEnd(ICheck check) + : base(check, IssueType.Problem, "Break ends {0} ms late.") + { + } + + public Issue Create(double startTime, double diff) => new Issue(startTime, this, (int)diff); + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Warning, "Break is non-functional due to being less than 650ms.") + { + } + + public Issue Create(double startTime) => new Issue(startTime, this); + } + } +} From b3974b34e7cd519d6576b0f01c8788976789f35c Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Sun, 16 Jul 2023 23:03:30 +0300 Subject: [PATCH 286/555] Test break checks --- .../Editing/Checks/CheckBreaksTest.cs | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs index 664f72c5f8..39e414827a 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -1,10 +1,113 @@ // 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + namespace osu.Game.Tests.Editing.Checks { public class CheckBreaksTest { - + private CheckBreaks check = null!; + + [SetUp] + public void Setup() + { + check = new CheckBreaks(); + } + + [Test] + public void TestBreakTooShort() + { + var beatmap = new Beatmap + { + Breaks = new List + { + new BreakPeriod(0, 649) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateTooShort); + } + + [Test] + public void TestBreakStartsEarly() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_200 } + }, + Breaks = new List + { + new BreakPeriod(100, 751) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart); + } + + [Test] + public void TestBreakEndsLate() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_298 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); + } + + [Test] + public void TestBreaksCorrect() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_300 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Is.Empty); + } } } From 17aac0694eec257217bb8f546806d9021cb97147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jul 2023 19:19:03 +0200 Subject: [PATCH 287/555] Re-enable connection retrying on discord connector --- osu.Desktop/DiscordRichPresence.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index b1e11d7a60..caf0a1d9fd 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -54,9 +54,6 @@ namespace osu.Desktop client.OnReady += onReady; - // safety measure for now, until we performance test / improve backoff for failed connections. - client.OnConnectionFailed += (_, _) => client.Deinitialize(); - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); From ff529d9df7d66dc3191c6f75b3691df451c7aeb9 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Mon, 17 Jul 2023 20:48:53 +0300 Subject: [PATCH 288/555] Rename variables, fix check message formatting --- osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs index 12dc5554f4..54dfb557fe 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Edit.Checks { // Breaks may be off by 1 ms. private const int leniency_threshold = 1; - private const double min_start_threshold = 200; + private const double minimum_gap_before_break = 200; // Break end time depends on the upcoming object's pre-empt time. // As things stand, "pre-empt time" is only defined for osu! standard @@ -40,13 +40,13 @@ namespace osu.Game.Rulesets.Edit.Checks { foreach (var breakPeriod in context.Beatmap.Breaks) { - double diffStart = breakPeriod.StartTime - hitObject.GetEndTime(); - double diffEnd = hitObject.StartTime - breakPeriod.EndTime; + double gapBeforeBreak = breakPeriod.StartTime - hitObject.GetEndTime(); + double gapAfterBreak = hitObject.StartTime - breakPeriod.EndTime; - if (diffStart < min_start_threshold - leniency_threshold && diffStart > 0) - yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, min_start_threshold - diffStart); - else if (diffEnd < min_end_threshold - leniency_threshold && diffEnd > 0) - yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - diffEnd); + if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold && gapBeforeBreak > 0) + yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); + else if (gapAfterBreak < min_end_threshold - leniency_threshold && gapAfterBreak > 0) + yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); } } } @@ -74,11 +74,11 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateTooShort : IssueTemplate { public IssueTemplateTooShort(ICheck check) - : base(check, IssueType.Warning, "Break is non-functional due to being less than 650ms.") + : base(check, IssueType.Warning, "Break is non-functional due to being less than {0} ms.") { } - public Issue Create(double startTime) => new Issue(startTime, this); + public Issue Create(double startTime) => new Issue(startTime, this, BreakPeriod.MIN_BREAK_DURATION); } } } From e6e66c6aefa197732de8b0d425cc736870332147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 13:08:23 +0900 Subject: [PATCH 289/555] Remove mention of clock being nullable in `IBeatSyncProvider` Co-authored-by: Susko3 --- osu.Game/Beatmaps/IBeatSyncProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatSyncProvider.cs b/osu.Game/Beatmaps/IBeatSyncProvider.cs index 776552cfa5..61fcf7f8e2 100644 --- a/osu.Game/Beatmaps/IBeatSyncProvider.cs +++ b/osu.Game/Beatmaps/IBeatSyncProvider.cs @@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps ControlPointInfo? ControlPoints { get; } /// - /// Access a clock currently responsible for providing beat sync. If null, no current provider is available. + /// Access a clock currently responsible for providing beat sync. /// IClock Clock { get; } } From 49bb0b190a6531a8f65bccec747747ae31858f27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 16:14:54 +0900 Subject: [PATCH 290/555] Split out star constant to reuse in pool definition --- osu.Game/Screens/Menu/StarFountain.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 7fa996e39a..3cfa529d40 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens.Menu { public partial class StarFountain : CompositeDrawable { + private const int stars_per_shoot = 192; + private DrawablePool starPool = null!; private Container starContainer = null!; @@ -21,7 +23,7 @@ namespace osu.Game.Screens.Menu { InternalChildren = new Drawable[] { - starPool = new DrawablePool(192), + starPool = new DrawablePool(stars_per_shoot), starContainer = new Container() }; } @@ -31,8 +33,6 @@ namespace osu.Game.Screens.Menu // left centre or right movement. int direction = RNG.Next(-1, 2); - const int total_stars = 192; - const float x_velocity_from_direction = 0.6f; const float x_velocity_random_variance = 0.25f; @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Menu const float x_spawn_position_variance = 10; const float y_spawn_position_offset = 50; - for (int i = 0; i < total_stars; i++) + for (int i = 0; i < stars_per_shoot; i++) { double initialOffset = i * 3; From f4acc86df8ad3ea0d36b78bc2a7603322f954a36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 16:26:37 +0900 Subject: [PATCH 291/555] Adjust metrics to closer match expectations --- osu.Game/Screens/Menu/StarFountain.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 3cfa529d40..3b4fdd7abf 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -33,30 +33,30 @@ namespace osu.Game.Screens.Menu // left centre or right movement. int direction = RNG.Next(-1, 2); - const float x_velocity_from_direction = 0.6f; - const float x_velocity_random_variance = 0.25f; + const float x_velocity_from_direction = 0.8f; + const float x_velocity_random_variance = 0.15f; const float y_velocity_base = -2.0f; - const float y_velocity_random_variance = 0.25f; + const float y_velocity_random_variance = 0.15f; const float x_spawn_position_variance = 10; const float y_spawn_position_offset = 50; for (int i = 0; i < stars_per_shoot; i++) { - double initialOffset = i * 3; + double capturedIndex = i; starContainer.Add(starPool.Get(s => { s.Velocity = new Vector2( - direction * x_velocity_from_direction + getRandomVariance(x_velocity_random_variance), + direction * x_velocity_from_direction * (1 - 2 * ((float)capturedIndex / stars_per_shoot)) + getRandomVariance(x_velocity_random_variance), y_velocity_base + getRandomVariance(y_velocity_random_variance)); s.Position = new Vector2(getRandomVariance(x_spawn_position_variance), y_spawn_position_offset); s.Hide(); - using (s.BeginDelayedSequence(initialOffset)) + using (s.BeginDelayedSequence(capturedIndex * 3)) { double duration = RNG.Next(300, 1300); From c02684d985e426b8dab1bf175204026cff2845e3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:18:43 +0200 Subject: [PATCH 292/555] truncate hit object end time --- osu.Game/Database/LegacyBeatmapExporter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 42d8a72073..3b2282c234 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -60,6 +60,9 @@ namespace osu.Game.Database { hitObject.StartTime = Math.Floor(hitObject.StartTime); + if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath) + hasDuration.Duration = Math.Floor(hasDuration.Duration); + if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); From bcdf5310390021d2b5b4996c6d88293846a12004 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:28:35 +0200 Subject: [PATCH 293/555] truncate end time before start time --- osu.Game/Database/LegacyBeatmapExporter.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 3b2282c234..983d25a25a 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -58,10 +58,11 @@ namespace osu.Game.Database foreach (var hitObject in beatmapContent.HitObjects) { - hitObject.StartTime = Math.Floor(hitObject.StartTime); - + // Truncate end time before truncating start time because end time is dependent on start time if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath) - hasDuration.Duration = Math.Floor(hasDuration.Duration); + hasDuration.Duration = Math.Floor(hasDuration.EndTime) - Math.Floor(hitObject.StartTime); + + hitObject.StartTime = Math.Floor(hitObject.StartTime); if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; From 395dd23966d0ae3fb15b13b8e0b8c6628e1a310b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:37:37 +0200 Subject: [PATCH 294/555] Put 'Export package' and 'Export legacy package' in one nested menu --- osu.Game/Localisation/EditorStrings.cs | 9 +++++++-- osu.Game/Screens/Edit/Editor.cs | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 70392d5c83..07b4dddc05 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -40,9 +40,14 @@ namespace osu.Game.Localisation public static LocalisableString ExportPackage => new TranslatableString(getKey(@"export_package"), @"Export package"); /// - /// "Export legacy package" + /// "Legacy format (.osz)" /// - public static LocalisableString ExportLegacyPackage => new TranslatableString(getKey(@"export_legacy_package"), @"Export legacy package"); + public static LocalisableString ExportLegacyFormat => new TranslatableString(getKey(@"export_legacy_format"), @"Legacy format (.osz)"); + + /// + /// "New format (.olz)" + /// + public static LocalisableString ExportNewFormat => new TranslatableString(getKey(@"export_new_format"), @"New format (.olz)"); /// /// "Create new difficulty" diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a48c57f991..16cd7768fa 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -998,8 +998,7 @@ namespace osu.Game.Screens.Edit private List createFileMenuItems() => new List { new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(EditorStrings.ExportPackage, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new EditorMenuItem(EditorStrings.ExportLegacyPackage, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + createExportPackageMenu(), new EditorMenuItemSpacer(), createDifficultyCreationMenu(), createDifficultySwitchMenu(), @@ -1009,6 +1008,17 @@ namespace osu.Game.Screens.Edit new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit) }; + private EditorMenuItem createExportPackageMenu() + { + var exportItems = new List + { + new EditorMenuItem(EditorStrings.ExportNewFormat, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(EditorStrings.ExportLegacyFormat, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + }; + + return new EditorMenuItem(EditorStrings.ExportPackage) { Items = exportItems }; + } + private void exportBeatmap() { Save(); From 63dd8bd991835344cbbf3e0c88b8f4b749ea1029 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 12:40:48 +0200 Subject: [PATCH 295/555] use base.GetFileContents to get file stream --- osu.Game/Database/LegacyBeatmapExporter.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 983d25a25a..b90ea73244 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -8,7 +8,6 @@ using System.Text; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; -using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -36,7 +35,7 @@ namespace osu.Game.Database return base.GetFileContents(model, file); // Read the beatmap contents and skin - using var contentStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + using var contentStream = base.GetFileContents(model, file); if (contentStream == null) return null; @@ -44,7 +43,7 @@ namespace osu.Game.Database using var contentStreamReader = new LineBufferedReader(contentStream); var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); - using var skinStream = UserFileStorage.GetStream(file.File.GetStoragePath()); + using var skinStream = base.GetFileContents(model, file); using var skinStreamReader = new LineBufferedReader(contentStream); var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { From ccbb30cdda8370ad626b30a56d1d9c7f169d03ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 19:52:45 +0900 Subject: [PATCH 296/555] Fix `ParticleSpewer` not correctly accounting for lower frame rates (and spawning less particles) --- osu.Game/Graphics/ParticleSpewer.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 8519cf0c59..02053e1be7 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -20,9 +20,9 @@ namespace osu.Game.Graphics { private readonly FallingParticle[] particles; private int currentIndex; - private double lastParticleAdded; + private double? lastParticleAdded; - private readonly double cooldown; + private readonly double timeBetweenSpawns; private readonly double maxDuration; /// @@ -44,7 +44,7 @@ namespace osu.Game.Graphics particles = new FallingParticle[perSecond * (int)Math.Ceiling(maxDuration / 1000)]; - cooldown = 1000f / perSecond; + timeBetweenSpawns = 1000f / perSecond; this.maxDuration = maxDuration; } @@ -52,18 +52,27 @@ namespace osu.Game.Graphics { base.Update(); - if (Active.Value && CanSpawnParticles && Math.Abs(Time.Current - lastParticleAdded) > cooldown) + Invalidate(Invalidation.DrawNode); + + if (!Active.Value || !CanSpawnParticles) { + lastParticleAdded = null; + return; + } + + while (lastParticleAdded == null || Math.Abs(Time.Current - lastParticleAdded.Value) > timeBetweenSpawns) + { + lastParticleAdded ??= Time.Current; + var newParticle = CreateParticle(); - newParticle.StartTime = (float)Time.Current; + newParticle.StartTime = (float)lastParticleAdded; particles[currentIndex] = newParticle; currentIndex = (currentIndex + 1) % particles.Length; - lastParticleAdded = Time.Current; - } - Invalidate(Invalidation.DrawNode); + lastParticleAdded += timeBetweenSpawns; + } } /// From ba237a0e3aad8e37b4a75f2a0d79afdf51005e58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jul 2023 19:52:58 +0900 Subject: [PATCH 297/555] Convert `StarFountain` to use `ParticleSpewer` More efficient --- osu.Game/Screens/Menu/StarFountain.cs | 129 +++++++++++--------------- 1 file changed, 54 insertions(+), 75 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 3b4fdd7abf..debf0007eb 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -2,113 +2,92 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Menu { - public partial class StarFountain : CompositeDrawable + public partial class StarFountain : SkinReloadableDrawable { - private const int stars_per_shoot = 192; + private StarFountainSpewer spewer = null!; - private DrawablePool starPool = null!; - private Container starContainer = null!; + [Resolved] + private TextureStore textures { get; set; } = null!; [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] - { - starPool = new DrawablePool(stars_per_shoot), - starContainer = new Container() - }; + InternalChild = spewer = new StarFountainSpewer(); } - public void Shoot() + public void Shoot() => spewer.Shoot(); + + protected override void SkinChanged(ISkinSource skin) { - // left centre or right movement. - int direction = RNG.Next(-1, 2); + base.SkinChanged(skin); + spewer.Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); + } - const float x_velocity_from_direction = 0.8f; - const float x_velocity_random_variance = 0.15f; + public partial class StarFountainSpewer : ParticleSpewer + { + private const int particle_duration_min = 300; + private const int particle_duration_max = 1000; - const float y_velocity_base = -2.0f; - const float y_velocity_random_variance = 0.15f; + private double? lastShootTime; + private int lastShootDirection; - const float x_spawn_position_variance = 10; - const float y_spawn_position_offset = 50; + protected override float ParticleGravity => 800; - for (int i = 0; i < stars_per_shoot; i++) + private const double shoot_duration = 800; + + protected override bool CanSpawnParticles => lastShootTime != null && Time.Current - lastShootTime < shoot_duration; + + [Resolved] + private ISkinSource skin { get; set; } = null!; + + public StarFountainSpewer() + : base(null, 240, particle_duration_max) { - double capturedIndex = i; - - starContainer.Add(starPool.Get(s => - { - s.Velocity = new Vector2( - direction * x_velocity_from_direction * (1 - 2 * ((float)capturedIndex / stars_per_shoot)) + getRandomVariance(x_velocity_random_variance), - y_velocity_base + getRandomVariance(y_velocity_random_variance)); - - s.Position = new Vector2(getRandomVariance(x_spawn_position_variance), y_spawn_position_offset); - - s.Hide(); - - using (s.BeginDelayedSequence(capturedIndex * 3)) - { - double duration = RNG.Next(300, 1300); - - s.ScaleTo(1) - .ScaleTo(RNG.NextSingle(1, 2.8f), duration, Easing.Out) - .FadeOutFromOne(duration, Easing.Out) - .Expire(); - } - })); } - } - - private partial class Star : PoolableDrawable - { - public Vector2 Velocity = Vector2.Zero; - - private float rotation; [BackgroundDependencyLoader] - private void load() + private void load(TextureStore textures) { - AutoSizeAxes = Axes.Both; - Origin = Anchor.Centre; + Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); + Active.Value = true; + } - InternalChildren = new Drawable[] + protected override FallingParticle CreateParticle() + { + return new FallingParticle { - new SkinnableSprite("Menu/fountain-star") - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingParameters.Additive, - } + StartPosition = new Vector2(0, 50), + Duration = RNG.NextSingle(particle_duration_min, particle_duration_max), + StartAngle = getRandomVariance(4), + EndAngle = getRandomVariance(2), + EndScale = 1.4f + getRandomVariance(0.4f), + Velocity = new Vector2(getCurrentAngle(), -1200 + getRandomVariance(100)), }; - - rotation = getRandomVariance(2); } - protected override void Update() + private float getCurrentAngle() { - const float gravity = 0.003f; + const float x_velocity_from_direction = 500; + const float x_velocity_random_variance = 60; - base.Update(); - - float elapsed = (float)Time.Elapsed; - - Position += Velocity * elapsed; - Velocity += new Vector2(0, elapsed * gravity); - - Rotation += rotation * elapsed; + return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); } - } - private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); + public void Shoot() + { + lastShootTime = Clock.CurrentTime; + lastShootDirection = RNG.Next(-1, 2); + } + + private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); + } } } From e7a9175aeae253a515c8a8ab0d40985853de28ec Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 18 Jul 2023 13:08:05 +0200 Subject: [PATCH 298/555] fix skin using wrong stream --- osu.Game/Database/LegacyBeatmapExporter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index b90ea73244..e054652efa 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -44,7 +44,11 @@ namespace osu.Game.Database var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); using var skinStream = base.GetFileContents(model, file); - using var skinStreamReader = new LineBufferedReader(contentStream); + + if (skinStream == null) + return null; + + using var skinStreamReader = new LineBufferedReader(skinStream); var beatmapSkin = new LegacySkin(new SkinInfo(), null!) { Configuration = new LegacySkinDecoder().Decode(skinStreamReader) From 2e3d1fe950d292e6764715c892aa9ad0759d94b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 13:38:04 +0900 Subject: [PATCH 299/555] Fix regression in time jumping behaviour --- .../Gameplay/TestSceneParticleSpewer.cs | 6 +-- osu.Game/Graphics/ParticleSpewer.cs | 47 ++++++++++++++----- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs index c73d57dc2b..8fb34883bb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs @@ -69,13 +69,13 @@ namespace osu.Game.Tests.Visual.Gameplay spewer.Clock = new FramedClock(testClock); }); AddStep("start spewer", () => spewer.Active.Value = true); - AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1); + AddAssert("spawned first particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(1)); AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3); - AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2); + AddAssert("spawned second particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(2)); AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1); - AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3); + AddAssert("spawned third particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(3)); } private TestParticleSpewer createSpewer() => diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 02053e1be7..c761ec0b7e 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; @@ -60,19 +61,43 @@ namespace osu.Game.Graphics return; } - while (lastParticleAdded == null || Math.Abs(Time.Current - lastParticleAdded.Value) > timeBetweenSpawns) + // Always want to spawn the first particle in an activation immediately. + if (lastParticleAdded == null) { - lastParticleAdded ??= Time.Current; - - var newParticle = CreateParticle(); - newParticle.StartTime = (float)lastParticleAdded; - - particles[currentIndex] = newParticle; - - currentIndex = (currentIndex + 1) % particles.Length; - - lastParticleAdded += timeBetweenSpawns; + lastParticleAdded = Time.Current; + spawnParticle(); } + + double timeElapsed = Math.Abs(Time.Current - lastParticleAdded.Value); + + // Avoid spawning too many particles if a long amount of time has passed. + if (timeElapsed > maxDuration) + { + lastParticleAdded = Time.Current; + spawnParticle(); + return; + } + + Debug.Assert(lastParticleAdded != null); + + for (int i = 0; i < timeElapsed / timeBetweenSpawns; i++) + { + lastParticleAdded += timeBetweenSpawns; + spawnParticle(); + } + } + + private void spawnParticle() + { + Debug.Assert(lastParticleAdded != null); + + var newParticle = CreateParticle(); + + newParticle.StartTime = (float)lastParticleAdded.Value; + + particles[currentIndex] = newParticle; + + currentIndex = (currentIndex + 1) % particles.Length; } /// From 7fa95f7512f8b7737c1d569dcb2158b73939c89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 13:52:43 +0900 Subject: [PATCH 300/555] Fix `#region` --- osu.Game/Graphics/ParticleSpewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index c761ec0b7e..9cf3fb1882 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -107,7 +107,7 @@ namespace osu.Game.Graphics protected override DrawNode CreateDrawNode() => new ParticleSpewerDrawNode(this); - # region DrawNode + #region DrawNode private class ParticleSpewerDrawNode : SpriteDrawNode { From 5e2a0bd73302f2f6be74bb9792d0e067f1c092ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 14:10:27 +0900 Subject: [PATCH 301/555] Fix spawning edge case when elapsed became unexpectedly negative --- osu.Game/Graphics/ParticleSpewer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 9cf3fb1882..37a4fe77bd 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -66,12 +66,13 @@ namespace osu.Game.Graphics { lastParticleAdded = Time.Current; spawnParticle(); + return; } - double timeElapsed = Math.Abs(Time.Current - lastParticleAdded.Value); + double timeElapsed = Time.Current - lastParticleAdded.Value; // Avoid spawning too many particles if a long amount of time has passed. - if (timeElapsed > maxDuration) + if (Math.Abs(timeElapsed) > maxDuration) { lastParticleAdded = Time.Current; spawnParticle(); From b9a66ad7b346aa26d49a875a522deb8932b8dca9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 14:58:45 +0900 Subject: [PATCH 302/555] Add test coverage of incorrect selection behaviour --- .../Editor/TestSceneOsuComposerSelection.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index 8641663ce8..623cefff6b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + [Test] + public void TestSelectAfterFadedOut() + { + var slider = new Slider + { + StartTime = 0, + Position = new Vector2(100, 100), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + } + } + }; + AddStep("add slider", () => EditorBeatmap.Add(slider)); + + moveMouseToObject(() => slider); + + AddStep("seek after end", () => EditorClock.Seek(750)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); + + AddStep("seek to visible", () => EditorClock.Seek(650)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider); + } + [Test] public void TestContextMenuShownCorrectlyForSelectedSlider() { From 4a6a5b174a3255b8479edb2ab4683fd779040925 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 14:52:31 +0900 Subject: [PATCH 303/555] Fix editor blueprints being selectable for too long when hit markers are enabled Addresses https://github.com/ppy/osu/discussions/24163. --- .../Edit/Blueprints/OsuSelectionBlueprint.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index bdd19f9106..178b809d8b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -22,7 +22,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints protected override bool AlwaysShowWhenSelected => true; protected override bool ShouldBeAlive => base.ShouldBeAlive - || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime + && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + + public override bool HandlePositionalInput => + // Bypass fade out extension from hit markers for input handling purposes. + // This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable. + // + // Note that we are intentionally overriding HandlePositionalInput here and not ReceivePositionalInputAt + // as individual blueprint implementations override that. + base.ShouldBeAlive; protected OsuSelectionBlueprint(T hitObject) : base(hitObject) From 871056790b4bfb56fb5d5fad4e3e224673b53987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:01:20 +0900 Subject: [PATCH 304/555] Mark editor tile as non-localisable --- osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index fa4e52d6a6..e958849bb0 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -61,8 +61,8 @@ namespace osu.Game.Screens.Edit.Components.Menus }, }); - text.AddText("osu!", t => t.Font = OsuFont.TorusAlternate); - text.AddText("editor", t => + text.AddText(@"osu!", t => t.Font = OsuFont.TorusAlternate); + text.AddText(@"editor", t => { t.Font = OsuFont.TorusAlternate; t.Colour = colourProvider.Highlight1; From 55a41b0887ff6e7d78502f40dc61204eda4c0306 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:13:20 +0900 Subject: [PATCH 305/555] Fix overlap between header text and menu items --- osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index e958849bb0..b9385ff0c3 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -25,7 +25,10 @@ namespace osu.Game.Screens.Edit.Components.Menus RelativeSizeAxes = Axes.X; MaskingContainer.CornerRadius = 0; - ItemsContainer.Padding = new MarginPadding { Left = heading_area }; + ItemsContainer.Padding = new MarginPadding(); + + ContentContainer.Margin = new MarginPadding { Left = heading_area }; + ContentContainer.Masking = true; } [BackgroundDependencyLoader] From 4e4dcc9846cdb1c9c052b021d3d90681ce877bff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 16:32:04 +0900 Subject: [PATCH 306/555] Add test coverage of selection preferring closest objects --- .../Editing/TestSceneComposerSelection.cs | 75 ++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index d6934a3770..be5dd59206 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -6,21 +6,21 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Tests.Beatmaps; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Input; @@ -217,6 +217,75 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); } + [Test] + public void TestNearestSelection() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(100)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek near second", () => EditorClock.Seek(500)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek halfway", () => EditorClock.Seek(300)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestNearestSelectionWithEndTime() + { + var firstObject = new Slider + { + Position = new Vector2(256, 192), + StartTime = 0, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(50, 0)), + }) + }; + + var secondObject = new HitCircle + { + Position = new Vector2(256, 192), + StartTime = 600 + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new HitObject[] { firstObject, secondObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(100)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek near second", () => EditorClock.Seek(500)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek roughly halfway", () => EditorClock.Seek(350)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + // Slider gets priority due to end time. + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + [TestCase(false)] [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) From 5ade093c5a70f32971ed26422c7c53f29b6529bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:55:38 +0900 Subject: [PATCH 307/555] Change editor to always perform selection of closest object --- .../Compose/Components/BlueprintContainer.cs | 27 ++++++++++++++----- .../Components/EditorBlueprintContainer.cs | 5 ++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 56a6b18433..5fdd2634c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -381,6 +381,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool selectedBlueprintAlreadySelectedOnMouseDown; + protected virtual IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => blueprints.Reverse(); + /// /// Attempts to select any hovered blueprints. /// @@ -390,15 +392,28 @@ namespace osu.Game.Screens.Edit.Compose.Components { // Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Priority is given to already-selected blueprints. - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected)) + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsSelected)) { - if (!blueprint.IsHovered) continue; + if (runForBlueprint(blueprint)) + return true; + } - selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected; - return clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e); + foreach (SelectionBlueprint blueprint in ApplySelectionOrder(SelectionBlueprints.AliveChildren)) + { + if (runForBlueprint(blueprint)) + return true; } return false; + + bool runForBlueprint(SelectionBlueprint blueprint) + { + if (!blueprint.IsHovered) return false; + + selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected; + clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e); + return true; + } } /// @@ -432,13 +447,13 @@ namespace osu.Game.Screens.Edit.Compose.Components // The depth of blueprints is constantly changing (see above where selected blueprints are brought to the front). // For this logic, we want a stable sort order so we can correctly cycle, thus using the blueprintMap instead. - IEnumerable> cyclingSelectionBlueprints = blueprintMap.Values; + IEnumerable> cyclingSelectionBlueprints = ApplySelectionOrder(blueprintMap.Values); // If there's already a selection, let's start from the blueprint after the selection. cyclingSelectionBlueprints = cyclingSelectionBlueprints.SkipWhile(b => !b.IsSelected).Skip(1); // Add the blueprints from before the selection to the end of the enumerable to allow for cyclic selection. - cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(blueprintMap.Values.TakeWhile(b => !b.IsSelected)); + cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(ApplySelectionOrder(blueprintMap.Values).TakeWhile(b => !b.IsSelected)); foreach (SelectionBlueprint blueprint in cyclingSelectionBlueprints) { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 65797a968d..ad0e8b124b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -129,6 +130,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } + protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => + base.ApplySelectionOrder(blueprints) + .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); + protected override Container> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; protected override SelectionHandler CreateSelectionHandler() => new EditorSelectionHandler(); From 9d46d00294cda3ea10fca9507d008975efcc91d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 16:17:42 +0900 Subject: [PATCH 308/555] Update skin editor cyclic test to match expectations better --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 7b37b6624d..4e5db5d46e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -138,24 +138,28 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestCyclicSelection() { - SkinBlueprint[] blueprints = null!; + List blueprints = new List(); - AddStep("Add big black boxes", () => + AddStep("clear list", () => blueprints.Clear()); + + for (int i = 0; i < 3; i++) { - InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); - }); + AddStep("Add big black box", () => + { + InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("store box", () => + { + // Add blueprints one-by-one so we have a stable order for testing reverse cyclic selection against. + blueprints.Add(skinEditor.ChildrenOfType().Single(s => s.IsSelected)); + }); + } AddAssert("Three black boxes added", () => targetContainer.Components.OfType().Count(), () => Is.EqualTo(3)); - AddStep("Store black box blueprints", () => - { - blueprints = skinEditor.ChildrenOfType().Where(b => b.Item is BigBlackBox).ToArray(); - }); - - AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); + AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); AddStep("move cursor to black box", () => { @@ -164,13 +168,13 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 2", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item)); + AddAssert("Selection is second last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 3", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); + AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); + AddAssert("Selection is first", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); AddStep("select all boxes", () => { From 5ec9cd84b25631ba0bc8ed2c0efcadc331a60afe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:13:19 +0900 Subject: [PATCH 309/555] Change offset calibration control to adjust for all difficulties of the current beatmap set --- .../Play/PlayerSettings/BeatmapOffsetControl.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index b542707185..840077eb7f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -162,17 +162,20 @@ namespace osu.Game.Screens.Play.PlayerSettings realmWriteTask = realm.WriteAsync(r => { - var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; + var setInfo = r.Find(beatmap.Value.BeatmapSetInfo.ID); - if (settings == null) // only the case for tests. + if (setInfo == null) // only the case for tests. return; - double val = Current.Value; + // Apply to all difficulties in a beatmap set for now (they generally always share timing). + foreach (var b in setInfo.Beatmaps) + { + BeatmapUserSettings settings = b.UserSettings; + double val = Current.Value; - if (settings.Offset == val) - return; - - settings.Offset = val; + if (settings.Offset != val) + settings.Offset = val; + } }); } } From d33b174243ef6fe98b3c9ac64cfb6f4ccc91d91c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:21:17 +0900 Subject: [PATCH 310/555] Add test coverage of beatmap editor cyclic selection --- .../Editing/TestSceneComposerSelection.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index be5dd59206..d7fb13390f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -286,6 +286,85 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); } + [Test] + public void TestCyclicSelection() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestCyclicSelectionOutwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(320)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + } + + [Test] + public void TestCyclicSelectionBackwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(600)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + } + [TestCase(false)] [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) From cf3949c9e21b3fe4f085a8a43d56b60ecdde5ae6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 15:35:40 +0900 Subject: [PATCH 311/555] Fix double-click handling when cyclic selection is enabled Removes the limitations of cyclic selection as a result. --- .../SkinEditor/SkinBlueprintContainer.cs | 2 -- .../Compose/Components/BlueprintContainer.cs | 20 +++++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index db27e20010..3f8d9f80d4 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -25,8 +25,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor editor { get; set; } = null!; - protected override bool AllowCyclicSelection => true; - public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer) { this.targetContainer = targetContainer; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5fdd2634c4..c8df999d37 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -46,15 +46,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly BindableList SelectedItems = new BindableList(); - /// - /// Whether to allow cyclic selection on clicking multiple times. - /// - /// - /// Disabled by default as it does not work well with editors that support double-clicking or other advanced interactions. - /// Can probably be made to work with more thought. - /// - protected virtual bool AllowCyclicSelection => false; - protected BlueprintContainer() { RelativeSizeAxes = Axes.Both; @@ -167,6 +158,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (ClickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != ClickedBlueprint) return false; + doubleClickHandled = true; return true; } @@ -177,6 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { endClickSelection(e); clickSelectionHandled = false; + doubleClickHandled = false; isDraggingBlueprint = false; wasDragStarted = false; }); @@ -376,6 +369,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool clickSelectionHandled; + /// + /// Whether a blueprint was double-clicked since last mouse down. + /// + private bool doubleClickHandled; + /// /// Whether the selected blueprint(s) were already selected on mouse down. Generally used to perform selection cycling on mouse up in such a case. /// @@ -424,7 +422,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool endClickSelection(MouseButtonEvent e) { // If already handled a selection or drag, we don't want to perform a mouse up / click action. - if (clickSelectionHandled || isDraggingBlueprint) return true; + if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint) return true; if (e.Button != MouseButton.Left) return false; @@ -440,7 +438,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1 && AllowCyclicSelection) + if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) { // If a click occurred and was handled by the currently selected blueprint but didn't result in a drag, // cycle between other blueprints which are also under the cursor. From 94c5b8ed32a322d58b04b894eb65449b81bb9bf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:22:25 +0900 Subject: [PATCH 312/555] Apply NRT to `TestSceneComposerSelection` --- .../Visual/Editing/TestSceneComposerSelection.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index d7fb13390f..51e75939fe 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; @@ -82,7 +80,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestNudgeSelection() { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -104,7 +102,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestRotateHotkeys() { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -136,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestGlobalFlipHotkeys() { - HitCircle addedObject = null; + HitCircle addedObject = null!; AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 })); @@ -369,7 +367,7 @@ namespace osu.Game.Tests.Visual.Editing [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -468,7 +466,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestQuickDeleteRemovesSliderControlPoint() { - Slider slider = null; + Slider slider = null!; PathControlPoint[] points = { From e283845b7144d1e16039841af85f3441a3be2a26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:45:07 +0900 Subject: [PATCH 313/555] Adjust legacy skin elements to better align with skinning expectations --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 4 ++-- osu.Game/Skinning/LegacyScoreCounter.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index c99cdba91c..326257c25f 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -17,8 +17,8 @@ namespace osu.Game.Skinning Anchor = Anchor.TopRight; Origin = Anchor.TopRight; - Scale = new Vector2(0.6f); - Margin = new MarginPadding(10); + Scale = new Vector2(0.6f * 0.96f); + Margin = new MarginPadding { Vertical = 9, Horizontal = 17 }; } protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index d8ee6b21de..d238369be1 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -21,7 +21,7 @@ namespace osu.Game.Skinning Origin = Anchor.TopRight; Scale = new Vector2(0.96f); - Margin = new MarginPadding(10); + Margin = new MarginPadding { Horizontal = 10 }; } protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 79f13686e8..00c18bef3d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -368,7 +368,7 @@ namespace osu.Game.Skinning { songProgress.Anchor = Anchor.TopRight; songProgress.Origin = Anchor.CentreRight; - songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 10; + songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 20; songProgress.Y = container.ToLocalSpace(accuracy.ScreenSpaceDrawQuad.TopLeft).Y + (accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).Y / 2); } From 61ff3d08d45f2667215c46d1412cd9d3f02e3d5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 17:48:19 +0900 Subject: [PATCH 314/555] Change depth of `LegacySongProgress` to allow "skinning" via health bar background --- osu.Game/Skinning/LegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 00c18bef3d..b72a757e6c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -397,8 +397,8 @@ namespace osu.Game.Skinning new LegacyComboCounter(), new LegacyScoreCounter(), new LegacyAccuracyCounter(), - new LegacyHealthDisplay(), new LegacySongProgress(), + new LegacyHealthDisplay(), new BarHitErrorMeter(), new DefaultKeyCounterDisplay() } From eb149942e59b9995cfe31d0287289eb4842a0742 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:08:32 +0900 Subject: [PATCH 315/555] Add ability to toggle all free mods quickly at multiplayer song select --- .../TestSceneMultiplayerMatchSongSelect.cs | 8 ++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 + .../OnlinePlay/FooterButtonFreeMods.cs | 106 ++++++++++++++---- .../OnlinePlay/OnlinePlaySongSelect.cs | 9 +- 4 files changed, 104 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 947b7e5be6..8dc41cd707 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -67,6 +67,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } + [Test] + public void TestSelectFreeMods() + { + AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray()); + AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray()); + AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty()); + } + [Test] public void TestBeatmapConfirmed() { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c09668850a..ba3f01a688 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -111,6 +111,10 @@ namespace osu.Game.Overlays.Mods private readonly Bindable>> globalAvailableMods = new Bindable>>(); + public IEnumerable AllAvailableAndValidMods => allAvailableMods + .Select(s => s.Mod) + .Where(m => isValidMod(m)); + private IEnumerable allAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); private readonly BindableBool customisationVisible = new BindableBool(); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 920aff13a8..56a69be741 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -1,15 +1,21 @@ // 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.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osuTK; @@ -17,28 +23,60 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> { - public Bindable> Current + public Bindable> Current { get; set; } = new BindableWithCurrent>(); + + private OsuSpriteText count = null!; + + private Circle circle = null!; + + private readonly FreeModSelectOverlay freeModSelectOverlay; + + public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay) { - get => modDisplay.Current; - set => modDisplay.Current = value; + this.freeModSelectOverlay = freeModSelectOverlay; } - private readonly ModDisplay modDisplay; - - public FooterButtonFreeMods() - { - ButtonContentContainer.Add(modDisplay = new ModDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.8f), - ExpansionMode = ExpansionMode.AlwaysContracted, - }); - } + [Resolved] + private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { + ButtonContentContainer.AddRange(new[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + }, + count = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + UseFullGlyphHeight = false, + } + } + }, + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + Icon = FontAwesome.Solid.Bars, + Action = () => freeModSelectOverlay.ToggleVisibility() + } + }); + SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freemods"; @@ -49,14 +87,42 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateModDisplay(), true); + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + Action = toggleAllFreeMods; + } + + /// + /// Immediately toggle all free mods on/off. + /// + private void toggleAllFreeMods() + { + var availableMods = freeModSelectOverlay.AllAvailableAndValidMods.ToArray(); + + Current.Value = Current.Value.Count == availableMods.Length + ? Array.Empty() + : availableMods; } private void updateModDisplay() { - if (Current.Value?.Count > 0) - modDisplay.FadeIn(); + int current = Current.Value.Count; + + if (current == freeModSelectOverlay.AllAvailableAndValidMods.Count()) + { + count.Text = "all"; + circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); + } + else if (current > 0) + { + count.Text = $"{current} mods"; + circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); + } else - modDisplay.FadeOut(); + { + count.Text = "off"; + circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index e0ae437d49..622ffddba6 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -175,9 +175,12 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() { - var buttons = base.CreateFooterButtons().ToList(); - buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay)); - return buttons; + var baseButtons = base.CreateFooterButtons().ToList(); + var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods }; + + baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay)); + + return baseButtons; } /// From 310067b4c358c5fa5210b5990e6fd76dbefe14cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:11:19 +0900 Subject: [PATCH 316/555] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8febabb61b..4ea788a808 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 18aace177a7fdcd9a6756229326b102f669c7d53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:36:51 +0900 Subject: [PATCH 317/555] Fix deadlock when logging out while at the create match screen Closes https://github.com/ppy/osu/issues/24275. --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 9 +++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 14 ++++---------- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs | 6 ++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6b68024393..6e126a928a 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -23,6 +23,7 @@ using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -76,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + [Resolved(canBeNull: true)] protected OnlinePlayScreen ParentScreen { get; private set; } @@ -284,6 +288,8 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + protected virtual bool IsConnected => api.State.Value == APIState.Online; + public override bool OnBackButton() { if (Room.RoomID.Value == null) @@ -356,6 +362,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (ExitConfirmed) return true; + if (!IsConnected) + return true; + if (dialogOverlay == null || Room.RoomID.Value != null || Room.Playlist.Count == 0) return true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 01f04b44c9..f5746ca96c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -17,7 +17,6 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -50,9 +49,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } - [Resolved] - private IAPIProvider api { get; set; } - [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -79,6 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer handleRoomLost(); } + protected override bool IsConnected => base.IsConnected && client.IsConnected.Value; + protected override Drawable CreateMainContent() => new Container { RelativeSizeAxes = Axes.Both, @@ -254,13 +252,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override bool OnExiting(ScreenExitEvent e) { - // the room may not be left immediately after a disconnection due to async flow, - // so checking the MultiplayerClient / IAPIAccess statuses is also required. - if (client.Room == null || !client.IsConnected.Value || api.State.Value != APIState.Online) - { - // room has not been created yet or we're offline; exit immediately. + // room has not been created yet or we're offline; exit immediately. + if (client.Room == null || !IsConnected) return base.OnExiting(e); - } if (!exitConfirmed && dialogOverlay != null) { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index c7b32131cf..b527bf98a2 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -15,8 +13,8 @@ namespace osu.Game.Screens.OnlinePlay public virtual string ShortTitle => Title; - [Resolved(CanBeNull = true)] - protected IRoomManager RoomManager { get; private set; } + [Resolved] + protected IRoomManager? RoomManager { get; private set; } protected OnlinePlaySubScreen() { From e47722565ae2c53ee38f156a027aa197e54e3c7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jul 2023 19:39:10 +0900 Subject: [PATCH 318/555] Clarify guard condition in `RoomSubScreen` --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6e126a928a..75b673cf1b 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -365,7 +365,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!IsConnected) return true; - if (dialogOverlay == null || Room.RoomID.Value != null || Room.Playlist.Count == 0) + bool hasUnsavedChanges = Room.RoomID.Value == null && Room.Playlist.Count > 0; + + if (dialogOverlay == null || !hasUnsavedChanges) return true; // if the dialog is already displayed, block exiting until the user explicitly makes a decision. From 764029bde1eda3976982238bf9d1cba6ebe56fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jul 2023 19:23:08 +0200 Subject: [PATCH 319/555] Fix nullability inspection --- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd4f35cdd4..4478179726 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // To work around this, temporarily remove the room and trigger an immediate listing poll. if (e.Last is MultiplayerMatchSubScreen match) { - RoomManager.RemoveRoom(match.Room); + RoomManager?.RemoveRoom(match.Room); ListingPollingComponent.PollImmediately(); } } From fa29c25097a178ad04a08c5c8ab26a3c918e847e Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 20 Jul 2023 00:32:35 +0300 Subject: [PATCH 320/555] Change check to use binary search --- osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 57 ++++++++++++++++---- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs index 54dfb557fe..44d2c18dad 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -34,23 +34,62 @@ namespace osu.Game.Rulesets.Edit.Checks { if (breakPeriod.Duration < BreakPeriod.MIN_BREAK_DURATION) yield return new IssueTemplateTooShort(this).Create(breakPeriod.StartTime); - } - foreach (var hitObject in context.Beatmap.HitObjects) - { - foreach (var breakPeriod in context.Beatmap.Breaks) + var previousObject = getPreviousObject(breakPeriod.StartTime, context.Beatmap.HitObjects); + var nextObject = getNextObject(breakPeriod.EndTime, context.Beatmap.HitObjects); + + if (previousObject != null) { - double gapBeforeBreak = breakPeriod.StartTime - hitObject.GetEndTime(); - double gapAfterBreak = hitObject.StartTime - breakPeriod.EndTime; - - if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold && gapBeforeBreak > 0) + double gapBeforeBreak = breakPeriod.StartTime - previousObject.GetEndTime(); + if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold) yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); - else if (gapAfterBreak < min_end_threshold - leniency_threshold && gapAfterBreak > 0) + } + + if (nextObject != null) + { + double gapAfterBreak = nextObject.StartTime - breakPeriod.EndTime; + if (gapAfterBreak < min_end_threshold - leniency_threshold) yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); } } } + private HitObject? getPreviousObject(double time, IReadOnlyList hitObjects) + { + int left = 0; + int right = hitObjects.Count - 1; + + while (left <= right) + { + int mid = left + (right - left) / 2; + + if (hitObjects[mid].GetEndTime() < time) + left = mid + 1; + else + right = mid - 1; + } + + return right >= 0 ? hitObjects[right] : null; + } + + private HitObject? getNextObject(double time, IReadOnlyList hitObjects) + { + int left = 0; + int right = hitObjects.Count - 1; + + while (left <= right) + { + int mid = left + (right - left) / 2; + + if (hitObjects[mid].StartTime <= time) + left = mid + 1; + else + right = mid - 1; + } + + return left < hitObjects.Count ? hitObjects[left] : null; + } + public class IssueTemplateEarlyStart : IssueTemplate { public IssueTemplateEarlyStart(ICheck check) From ce78bb549f2e5d9c09bf91402f55a7d26555f191 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 20 Jul 2023 00:32:54 +0300 Subject: [PATCH 321/555] Add test for multiple too early objects in break --- .../Editing/Checks/CheckBreaksTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs index 39e414827a..aaa536f9b9 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -88,6 +88,30 @@ namespace osu.Game.Tests.Editing.Checks Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); } + [Test] + public void TestBreakMultipleObjectsEarly() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_297 }, + new HitCircle { StartTime = 1_298 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); + } + [Test] public void TestBreaksCorrect() { From 18c5fc689f4dc799ab433eb041ca9c09482668ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 12:58:13 +0900 Subject: [PATCH 322/555] Don't expose such specific information from `ModSelectOverlay` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 14 +++++--------- .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ba3f01a688..7ec108e3ec 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -111,11 +111,7 @@ namespace osu.Game.Overlays.Mods private readonly Bindable>> globalAvailableMods = new Bindable>>(); - public IEnumerable AllAvailableAndValidMods => allAvailableMods - .Select(s => s.Mod) - .Where(m => isValidMod(m)); - - private IEnumerable allAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); + public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); private readonly BindableBool customisationVisible = new BindableBool(); @@ -386,7 +382,7 @@ namespace osu.Game.Overlays.Mods private void filterMods() { - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } @@ -411,7 +407,7 @@ namespace osu.Game.Overlays.Mods bool anyCustomisableModActive = false; bool anyModPendingConfiguration = false; - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) { anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any(); anyModPendingConfiguration |= modState.PendingConfiguration; @@ -468,7 +464,7 @@ namespace osu.Game.Overlays.Mods var newSelection = new List(); - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) { var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == modState.Mod.GetType()); @@ -495,7 +491,7 @@ namespace osu.Game.Overlays.Mods if (externalSelectionUpdateInProgress) return; - var candidateSelection = allAvailableMods.Where(modState => modState.Active.Value) + var candidateSelection = AllAvailableMods.Where(modState => modState.Active.Value) .Select(modState => modState.Mod) .ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 56a69be741..294c80677d 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay /// private void toggleAllFreeMods() { - var availableMods = freeModSelectOverlay.AllAvailableAndValidMods.ToArray(); + var availableMods = allAvailableAndValidMods.ToArray(); Current.Value = Current.Value.Count == availableMods.Length ? Array.Empty() @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay { int current = Current.Value.Count; - if (current == freeModSelectOverlay.AllAvailableAndValidMods.Count()) + if (current == allAvailableAndValidMods.Count()) { count.Text = "all"; circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); @@ -124,5 +124,9 @@ namespace osu.Game.Screens.OnlinePlay circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); } } + + private IEnumerable allAvailableAndValidMods => freeModSelectOverlay.AllAvailableMods + .Where(state => state.ValidForSelection.Value) + .Select(state => state.Mod); } } From c93d6a4008639b68a9f6508d6b5e46f9e800df9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 13:04:21 +0900 Subject: [PATCH 323/555] Invert text colour when freemods is enabled for better contrast --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 294c80677d..3825cf18b9 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -111,16 +111,19 @@ namespace osu.Game.Screens.OnlinePlay if (current == allAvailableAndValidMods.Count()) { count.Text = "all"; + count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); } else if (current > 0) { count.Text = $"{current} mods"; + count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); } else { count.Text = "off"; + count.FadeColour(colours.GrayF, 200, Easing.OutQuint); circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); } } From d9d055361aa956e5233d38c689aa9077033d104a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 17:49:23 +0900 Subject: [PATCH 324/555] More realm analytic disables --- .gitignore | 1 - osu.Game/FodyWeavers.xml | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 osu.Game/FodyWeavers.xml diff --git a/.gitignore b/.gitignore index 0c7a18b437..525b3418cd 100644 --- a/.gitignore +++ b/.gitignore @@ -339,6 +339,5 @@ inspectcode # Fody (pulled in by Realm) - schema file FodyWeavers.xsd -**/FodyWeavers.xml .idea/.idea.osu.Desktop/.idea/misc.xml \ No newline at end of file diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml new file mode 100644 index 0000000000..7ff486f40c --- /dev/null +++ b/osu.Game/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + From f791f21dcb3bc22c439838f2f7bee5d84310b757 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Jul 2023 20:05:35 +0900 Subject: [PATCH 325/555] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 059c5f26e7..ed97f609cc 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + - true $(NoWarn);MT7091 + + + true + + + + false + -all + ios-arm64 From 97075b0726f82cec03e4bb5a533e9e6d11bc8c1b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jul 2023 20:31:26 +0300 Subject: [PATCH 393/555] Fix iOS visual tests having unusual bundle identifiers --- osu.Game.Rulesets.Catch.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Mania.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Osu.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist | 4 ++-- osu.Game.Tests.iOS/Info.plist | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 5ace6c07f5..f87043e1d1 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Catch.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Catch-Tests-iOS + sh.ppy.catch-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index ff5dde856e..740036309f 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Mania.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Mania-Tests-iOS + sh.ppy.mania-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index 1e33f2ff16..7f489874e7 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Osu.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Osu-Tests-iOS + sh.ppy.osu-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index 76cb3c0db0..162ee75c22 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Taiko.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Taiko-Tests-iOS + sh.ppy.taiko-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index ac661f6263..d2d0583e46 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Tests-iOS + sh.ppy.osu-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file From ba8ebefb50dca3a31462ecf4b282036ada782991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 18:09:07 +0200 Subject: [PATCH 394/555] Add basic structure for new rotation handler --- .../Compose/Components/SelectionHandler.cs | 9 ++++++ .../Components/SelectionRotationHandler.cs | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9b44b15fe4..80df796fd7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] protected IEditorChangeHandler ChangeHandler { get; private set; } + protected SelectionRotationHandler RotationHandler { get; private set; } + protected SelectionHandler() { selectedBlueprints = new List>(); @@ -66,6 +68,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { + RotationHandler = CreateRotationHandler(); + InternalChild = SelectionBox = CreateSelectionBox(); SelectedItems.CollectionChanged += (_, _) => @@ -132,6 +136,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any items could be rotated. public virtual bool HandleRotation(float angle) => false; + /// + /// Creates the handler to use for rotation operations. + /// + public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); + /// /// Handles the selected items being scaled. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs new file mode 100644 index 0000000000..595edbb4fc --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// Base handler for editor rotation operations. + /// + public class SelectionRotationHandler + { + /// + /// Whether the rotation can currently be performed. + /// + public Bindable CanRotate { get; private set; } = new BindableBool(); + + public virtual void Begin() + { + } + + public virtual void Update(float rotation, Vector2 origin) + { + } + + public virtual void Commit() + { + } + } +} From ba904fd77bbb530b505817b32dbb31ac0e5baa1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:18:38 +0200 Subject: [PATCH 395/555] Migrate osu! rotation handling to `SelectionRotationHandler` --- .../Edit/OsuSelectionHandler.cs | 30 +----- .../Edit/OsuSelectionRotationHandler.cs | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 468d8ae9f5..1d46b8ff8a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -28,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } - /// - /// During a transform, the initial origin is stored so it can be used throughout the operation. - /// - private Vector2? referenceOrigin; - /// /// During a transform, the initial path types of a single selected slider are stored so they /// can be maintained throughout the operation. @@ -54,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Edit protected override void OnOperationEnded() { base.OnOperationEnded(); - referenceOrigin = null; referencePathTypes = null; } @@ -170,28 +164,10 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; } - public override bool HandleRotation(float delta) + public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(ChangeHandler) { - var hitObjects = selectedMovableObjects; - - Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); - - referenceOrigin ??= quad.Centre; - - foreach (var h in hitObjects) - { - h.Position = GeometryUtils.RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); - - if (h is IHasPath path) - { - foreach (PathControlPoint cp in path.Path.ControlPoints) - cp.Position = GeometryUtils.RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta); - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + SelectedItems = { BindTarget = SelectedItems } + }; private void scaleSlider(Slider slider, Vector2 scale) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs new file mode 100644 index 0000000000..0eb7637786 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -0,0 +1,98 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class OsuSelectionRotationHandler : SelectionRotationHandler + { + private readonly IEditorChangeHandler? changeHandler; + + public BindableList SelectedItems { get; } = new BindableList(); + + public OsuSelectionRotationHandler(IEditorChangeHandler? changeHandler) + { + this.changeHandler = changeHandler; + + SelectedItems.CollectionChanged += (_, __) => updateState(); + } + + private void updateState() + { + var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); + CanRotate.Value = quad.Width > 0 || quad.Height > 0; + } + + private OsuHitObject[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalPositions; + private Dictionary? originalPathControlPointPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = selectedMovableObjects.ToArray(); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre; + originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position); + originalPathControlPointPositions = objectsInRotation.OfType().ToDictionary( + obj => obj, + obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray()); + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + + Vector2 actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var ho in objectsInRotation) + { + ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation); + + if (ho is IHasPath withPath) + { + var originalPath = originalPathControlPointPositions[withPath]; + + for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i) + withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation); + } + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalPathControlPointPositions = null; + defaultOrigin = null; + } + + private IEnumerable selectedMovableObjects => SelectedItems.Cast() + .Where(h => h is not Spinner); + } +} From f8047d6ab6d96bf9c7b87fcf50b93e2b084da2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:48:39 +0200 Subject: [PATCH 396/555] Migrate skin element rotation handling to `SelectionRotationHandler` --- .../SkinEditor/SkinSelectionHandler.cs | 29 +----- .../SkinSelectionRotationHandler.cs | 94 +++++++++++++++++++ 2 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 4a1ddd9d69..bee973bea0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -26,31 +26,11 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor skinEditor { get; set; } = null!; - public override bool HandleRotation(float angle) + public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler(ChangeHandler) { - if (SelectedBlueprints.Count == 1) - { - // for single items, rotate around the origin rather than the selection centre. - ((Drawable)SelectedBlueprints.First().Item).Rotation += angle; - } - else - { - var selectionQuad = getSelectionQuad(); - - foreach (var b in SelectedBlueprints) - { - var drawableItem = (Drawable)b.Item; - - var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle); - updateDrawablePosition(drawableItem, rotatedPosition); - - drawableItem.Rotation += angle; - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + SelectedItems = { BindTarget = SelectedItems }, + UpdatePosition = updateDrawablePosition + }; public override bool HandleScale(Vector2 scale, Anchor anchor) { @@ -172,7 +152,6 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanRotate = true; SelectionBox.CanScaleX = true; SelectionBox.CanScaleY = true; SelectionBox.CanFlipX = true; diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs new file mode 100644 index 0000000000..e60e2b1e12 --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -0,0 +1,94 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Skinning; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Overlays.SkinEditor +{ + public class SkinSelectionRotationHandler : SelectionRotationHandler + { + private readonly IEditorChangeHandler? changeHandler; + + public BindableList SelectedItems { get; } = new BindableList(); + public Action UpdatePosition { get; init; } = null!; + + public SkinSelectionRotationHandler(IEditorChangeHandler? changeHandler) + { + this.changeHandler = changeHandler; + + SelectedItems.CollectionChanged += (_, __) => updateState(); + } + + private void updateState() + { + CanRotate.Value = SelectedItems.Count > 0; + } + + private Drawable[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalRotations; + private Dictionary? originalPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = SelectedItems.Cast().ToArray(); + originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); + originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null); + + if (objectsInRotation.Length == 1 && origin == null) + { + // for single items, rotate around the origin rather than the selection centre by default. + objectsInRotation[0].Rotation = originalRotations.Single().Value + rotation; + return; + } + + var actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var drawableItem in objectsInRotation) + { + var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation); + UpdatePosition(drawableItem, rotatedPosition); + + drawableItem.Rotation = originalRotations[drawableItem] + rotation; + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalRotations = null; + defaultOrigin = null; + } + } +} From 21df0e2d60824ea1e34cd88f60741484d251d049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:57:31 +0200 Subject: [PATCH 397/555] Migrate test to `SelectionRotationHandler` --- .../Editing/TestSceneComposeSelectBox.cs | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 7a0b3d0c1a..147488812e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -34,13 +36,12 @@ namespace osu.Game.Tests.Visual.Editing { RelativeSizeAxes = Axes.Both, - CanRotate = true, CanScaleX = true, CanScaleY = true, CanFlipX = true, CanFlipY = true, - OnRotation = handleRotation, + RotationHandler = new TestSelectionRotationHandler(() => selectionArea), OnScale = handleScale } } @@ -71,11 +72,48 @@ namespace osu.Game.Tests.Visual.Editing return true; } - private bool handleRotation(float angle) + private class TestSelectionRotationHandler : SelectionRotationHandler { - // kinda silly and wrong, but just showing that the drag handles work. - selectionArea.Rotation += angle; - return true; + private readonly Func getTargetContainer; + + public TestSelectionRotationHandler(Func getTargetContainer) + { + this.getTargetContainer = getTargetContainer; + + CanRotate.Value = true; + } + + [CanBeNull] + private Container targetContainer; + + private float? initialRotation; + + public override void Begin() + { + if (targetContainer != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + targetContainer = getTargetContainer(); + initialRotation = targetContainer!.Rotation; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + // kinda silly and wrong, but just showing that the drag handles work. + targetContainer.Rotation = initialRotation!.Value + rotation; + } + + public override void Commit() + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + targetContainer = null; + initialRotation = null; + } } [Test] From aec3ca250cc3301415d0ba38bc0058b2a2463205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 20:01:30 +0200 Subject: [PATCH 398/555] Migrate `SelectionHandler` to use `SelectionRotationHandler` --- .../Edit/OsuSelectionHandler.cs | 1 - .../Edit/Compose/Components/SelectionBox.cs | 41 ++++++++----------- .../Components/SelectionBoxRotationHandle.cs | 20 +++++---- .../Compose/Components/SelectionHandler.cs | 2 +- .../Components/SelectionRotationHandler.cs | 9 +++- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 1d46b8ff8a..1dfbf4179b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Edit Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); - SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0; SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 5d9fac739c..53442071b5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -22,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private const float button_padding = 5; - public Func? OnRotation; + public SelectionRotationHandler? RotationHandler { get; init; } public Func? OnScale; public Func? OnFlip; public Func? OnReverse; @@ -51,22 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private bool canRotate; - - /// - /// Whether rotation support should be enabled. - /// - public bool CanRotate - { - get => canRotate; - set - { - if (canRotate == value) return; - - canRotate = value; - recreate(); - } - } + private IBindable canRotate = new BindableBool(); private bool canScaleX; @@ -161,7 +147,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] - private void load() => recreate(); + private void load() + { + if (RotationHandler != null) + canRotate.BindTo(RotationHandler.CanRotate); + + canRotate.BindValueChanged(_ => recreate()); + recreate(); + } protected override bool OnKeyDown(KeyDownEvent e) { @@ -174,10 +167,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return CanReverse && reverseButton?.TriggerClick() == true; case Key.Comma: - return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true; case Key.Period: - return CanRotate && rotateClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true; } return base.OnKeyDown(e); @@ -254,14 +247,14 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanScaleY) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); - if (CanRotate) addRotationComponents(); + if (canRotate.Value) addRotationComponents(); if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() { - rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => OnRotation?.Invoke(-90)); - rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => OnRotation?.Invoke(90)); + rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => RotationHandler?.Rotate(-90)); + rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => RotationHandler?.Rotate(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); @@ -331,7 +324,7 @@ namespace osu.Game.Screens.Edit.Compose.Components var handle = new SelectionBoxRotationHandle { Anchor = anchor, - HandleRotate = angle => OnRotation?.Invoke(angle) + RotationHandler = RotationHandler }; handle.OperationStarted += operationStarted; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index c2a3f12efd..4107a09692 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; @@ -21,7 +22,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip { - public Action HandleRotate { get; set; } + [CanBeNull] + public SelectionRotationHandler RotationHandler { get; init; } public LocalisableString TooltipText { get; private set; } @@ -63,10 +65,10 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnDragStart(DragStartEvent e) { - bool handle = base.OnDragStart(e); - if (handle) - cumulativeRotation.Value = 0; - return handle; + if (RotationHandler == null) return false; + + RotationHandler.Begin(); + return true; } protected override void OnDrag(DragEvent e) @@ -99,7 +101,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { - base.OnDragEnd(e); + RotationHandler?.Commit(); + UpdateHoverState(); + cumulativeRotation.Value = null; rawCumulativeRotation = 0; TooltipText = default; @@ -116,14 +120,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void applyRotation(bool shouldSnap) { - float oldRotation = cumulativeRotation.Value ?? 0; - float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation); newRotation = (newRotation - 180) % 360 + 180; cumulativeRotation.Value = newRotation; - HandleRotate?.Invoke(newRotation - oldRotation); + RotationHandler?.Update(newRotation); TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 80df796fd7..31ad8fa3d7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnRotation = HandleRotation, + RotationHandler = RotationHandler, OnScale = HandleScale, OnFlip = HandleFlip, OnReverse = HandleReverse, diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 595edbb4fc..d5dd1d38d4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -16,11 +16,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotate { get; private set; } = new BindableBool(); + public void Rotate(float rotation, Vector2? origin = null) + { + Begin(); + Update(rotation, origin); + Commit(); + } + public virtual void Begin() { } - public virtual void Update(float rotation, Vector2 origin) + public virtual void Update(float rotation, Vector2? origin = null) { } From a201152b042e0dfb13a4abaca85def6b5fc23577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 20:09:31 +0200 Subject: [PATCH 399/555] Add xmldoc to `SelectionRotationHandler` --- .../Components/SelectionRotationHandler.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index d5dd1d38d4..6524f7fa35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -16,6 +16,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotate { get; private set; } = new BindableBool(); + /// + /// Performs a single, instant, atomic rotation operation. + /// + /// + /// This method is intended to be used in atomic contexts (such as when pressing a single button). + /// For continuous operations, see the -- flow. + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// public void Rotate(float rotation, Vector2? origin = null) { Begin(); @@ -23,14 +35,48 @@ namespace osu.Game.Screens.Edit.Compose.Components Commit(); } + /// + /// Begins a continuous rotation operation. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// public virtual void Begin() { } + /// + /// Updates a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// As such, the values of and supplied should be relative to the state of the objects being rotated + /// when was called, rather than instantaneous deltas. + /// + /// + /// For instantaneous, atomic operations, use the convenience method. + /// + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// public virtual void Update(float rotation, Vector2? origin = null) { } + /// + /// Ends a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// public virtual void Commit() { } From bf89fbcd817aab251797b40d8a4942103c360109 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jul 2023 16:38:37 +0900 Subject: [PATCH 400/555] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index ed97f609cc..7f15d9fafd 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - +